Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
212 changes: 212 additions & 0 deletions packages/use-dataloader/src/__tests__/usePaginatedDataLoader.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
import { act } from '@testing-library/react'
import { renderHook } from '@testing-library/react-hooks'
import React from 'react'
import DataLoaderProvider from '../DataLoaderProvider'
import { KEY_IS_NOT_STRING_ERROR } from '../constants'
import {
UsePaginatedDataLoaderConfig,
UsePaginatedDataLoaderMethodParams,
UsePaginatedDataLoaderResult,
} from '../types'
import usePaginatedDataLoader from '../usePaginatedDataLoader'

type UseDataLoaderHookProps = {
config: UsePaginatedDataLoaderConfig<unknown>
key: string
method: (params: UsePaginatedDataLoaderMethodParams) => Promise<unknown>
}

const PROMISE_TIMEOUT = 5

const initialProps = {
config: {
enabled: true,
},
key: 'test',
method: jest.fn(
({ page, perPage }: UsePaginatedDataLoaderMethodParams) =>
new Promise(resolve => {
setTimeout(() => resolve(`${page}-${perPage}`), PROMISE_TIMEOUT)
}),
),
}
// eslint-disable-next-line react/prop-types
const wrapper = ({ children }: { children?: React.ReactNode }) => (
<DataLoaderProvider>{children}</DataLoaderProvider>
)

describe('useDataLoader', () => {
test('should render correctly without options', async () => {
const { result, waitForNextUpdate } = renderHook<
UseDataLoaderHookProps,
UsePaginatedDataLoaderResult
>(props => usePaginatedDataLoader(props.key, props.method), {
initialProps,
wrapper,
})
expect(result.current.data).toStrictEqual({})
expect(result.current.pageData).toBe(undefined)
expect(result.current.isLoading).toBe(true)
await waitForNextUpdate()
expect(initialProps.method).toBeCalledTimes(1)
expect(result.current.isLoading).toBe(false)
expect(result.current.isSuccess).toBe(true)
expect(result.current.data).toStrictEqual({ 1: '1-1' })
expect(result.current.pageData).toBe('1-1')
})

test('should render correctly without request enabled then enable it', async () => {
const method = jest.fn(
() =>
new Promise(resolve => {
setTimeout(() => resolve(true), PROMISE_TIMEOUT)
}),
)
let enabled = false
const { rerender, result, waitForNextUpdate } = renderHook<
UseDataLoaderHookProps,
UsePaginatedDataLoaderResult
>(
props =>
usePaginatedDataLoader(props.key, props.method, {
enabled,
}),
{
initialProps: {
...initialProps,
key: 'test-not-enabled-then-reload',
method,
},
wrapper,
},
)
expect(result.current.pageData).toBe(undefined)
expect(result.current.isLoading).toBe(false)
expect(method).toBeCalledTimes(0)
enabled = true
rerender()
expect(method).toBeCalledTimes(1)
expect(result.current.pageData).toBe(undefined)
expect(result.current.isLoading).toBe(true)
await waitForNextUpdate()
expect(result.current.isLoading).toBe(false)
expect(result.current.isSuccess).toBe(true)
expect(result.current.pageData).toBe(true)
})

test('should render correctly without valid key', () => {
const { result } = renderHook<
UseDataLoaderHookProps,
UsePaginatedDataLoaderResult
>(props => usePaginatedDataLoader(props.key, props.method), {
initialProps: {
...initialProps,
// @ts-expect-error used because we test with bad key
key: 2,
},
wrapper,
})
expect(result.error?.message).toBe(KEY_IS_NOT_STRING_ERROR)
})

test('should render correctly with result null', async () => {
const { result, waitForNextUpdate } = renderHook<
UseDataLoaderHookProps,
UsePaginatedDataLoaderResult
>(props => usePaginatedDataLoader(props.key, props.method, props.config), {
initialProps: {
...initialProps,
key: 'test-3',
method: () =>
new Promise(resolve => {
setTimeout(() => resolve(null), PROMISE_TIMEOUT)
}),
},
wrapper,
})
expect(result.current.pageData).toBe(undefined)
expect(result.current.isLoading).toBe(true)
await waitForNextUpdate()
expect(result.current.pageData).toBe(undefined)
expect(result.current.isSuccess).toBe(true)
expect(result.current.isLoading).toBe(false)
})

test('should render correctly then change page', async () => {
const { result, waitForNextUpdate } = renderHook<
UseDataLoaderHookProps,
UsePaginatedDataLoaderResult
>(props => usePaginatedDataLoader(props.key, props.method), {
initialProps: {
...initialProps,
key: 'test-4',
},
wrapper,
})
expect(result.current.data).toStrictEqual({})
expect(result.current.pageData).toBe(undefined)
expect(result.current.isLoading).toBe(true)
await waitForNextUpdate()
expect(result.current.isLoading).toBe(false)
expect(result.current.isSuccess).toBe(true)
expect(result.current.data).toStrictEqual({ 1: '1-1' })
expect(result.current.pageData).toBe('1-1')

act(() => {
result.current.goToNextPage()
})
expect(result.current.page).toBe(2)
expect(result.current.pageData).toBe(undefined)
expect(result.current.isLoading).toBe(true)
await waitForNextUpdate()
expect(result.current.isLoading).toBe(false)
expect(result.current.isSuccess).toBe(true)
expect(result.current.data).toStrictEqual({ 1: '1-1', 2: '2-1' })
expect(result.current.pageData).toBe('2-1')
act(() => {
result.current.goToPreviousPage()
result.current.goToPreviousPage()
result.current.goToPreviousPage()
result.current.goToPreviousPage()
})
expect(result.current.page).toBe(1)
expect(result.current.pageData).toBe('1-1')
act(() => {
result.current.goToPage(2)
result.current.goToPage(-21)
result.current.goToPage(0)
})
expect(result.current.page).toBe(1)
expect(result.current.pageData).toBe('1-1')
})

test('should render correctly go to next page, change key and should be on page 1', async () => {
const hookProps = {
...initialProps,
key: 'test-5',
}
const { rerender, result, waitForNextUpdate } = renderHook<
UseDataLoaderHookProps,
UsePaginatedDataLoaderResult
>(props => usePaginatedDataLoader(props.key, props.method), {
initialProps: hookProps,
wrapper,
})
await waitForNextUpdate()
act(() => {
result.current.goToNextPage()
})
await waitForNextUpdate()
expect(result.current.data).toStrictEqual({ 1: '1-1', 2: '2-1' })
hookProps.key = 'test-5-bis'
rerender()
expect(result.current.isLoading).toBe(true)
expect(result.current.pageData).toBe(undefined)
expect(result.current.data).toStrictEqual({})
await waitForNextUpdate()
expect(result.current.data).toStrictEqual({ 1: '1-1' })
expect(result.current.pageData).toBe('1-1')
expect(result.current.isSuccess).toBe(true)
expect(result.current.isLoading).toBe(false)
})
})
1 change: 1 addition & 0 deletions packages/use-dataloader/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export {
useDataLoaderContext,
} from './DataLoaderProvider'
export { default as useDataLoader } from './useDataLoader'
export { default as usePaginatedDataLoader } from './usePaginatedDataLoader'
40 changes: 40 additions & 0 deletions packages/use-dataloader/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,43 @@ export interface UseDataLoaderResult<T = unknown> {
previousData?: T
reload: () => Promise<void>
}

/**
* Params send to the method
*/
export type UsePaginatedDataLoaderMethodParams = {
page: number
perPage: number
}

export type UsePaginatedDataLoaderConfig<T = unknown> = {
enabled?: boolean
initialData?: T
keepPreviousData?: boolean
onError?: OnErrorFn
onSuccess?: OnSuccessFn
pollingInterval?: number
/**
* Max time before data from previous success is considered as outdated (in millisecond)
*/
maxDataLifetime?: number
needPolling?: NeedPollingType
initialPage?: number
perPage?: number
}

export type UsePaginatedDataLoaderResult<T = unknown> = {
pageData?: T
data?: Record<number, T | undefined>
error?: Error
isError: boolean
isIdle: boolean
isLoading: boolean
isPolling: boolean
isSuccess: boolean
reload: () => Promise<void>
goToPage: (page: number) => void
goToNextPage: () => void
goToPreviousPage: () => void
page: number
}
108 changes: 108 additions & 0 deletions packages/use-dataloader/src/usePaginatedDataLoader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { useCallback, useEffect, useState } from 'react'
import { KEY_IS_NOT_STRING_ERROR } from './constants'
import {
PromiseType,
UsePaginatedDataLoaderConfig,
UsePaginatedDataLoaderMethodParams,
UsePaginatedDataLoaderResult,
} from './types'
import useDataLoader from './useDataLoader'

/**
* @param {string} baseFetchKey base key used to cache data. Hook append -page-X to that key for each page you load
* @param {() => PromiseType} method a method that return a promise
* @param {useDataLoaderConfig} config hook configuration
* @returns {useDataLoaderResult} hook result containing data, request state, and method to reload the data
*/
const usePaginatedDataLoader = <T>(
baseFetchKey: string,
method: (params: UsePaginatedDataLoaderMethodParams) => PromiseType<T>,
{
enabled = true,
initialData,
keepPreviousData = true,
onError,
onSuccess,
pollingInterval,
maxDataLifetime,
needPolling,
initialPage,
perPage = 1,
}: UsePaginatedDataLoaderConfig<T> = {},
): UsePaginatedDataLoaderResult<T> => {
if (typeof baseFetchKey !== 'string') {
throw new Error(KEY_IS_NOT_STRING_ERROR)
}

const [data, setData] = useState<Record<number, T | undefined>>({})
const [page, setPage] = useState<number>(initialPage ?? 1)

const pageMethod = useCallback(
() => method({ page, perPage }),
[method, page, perPage],
)
const {
data: pageData,
isError,
isIdle,
isLoading,
isPolling,
isSuccess,
reload,
error,
} = useDataLoader(`${baseFetchKey}-page-${page}`, pageMethod, {
enabled,
initialData,
keepPreviousData,
maxDataLifetime,
needPolling,
onError,
onSuccess,
pollingInterval,
})

const goToNextPage = useCallback(() => {
setPage(current => current + 1)
}, [])

const goToPreviousPage = useCallback(() => {
setPage(current => (current > 1 ? current - 1 : 1))
}, [])

const goToPage = useCallback((newPage: number) => {
setPage(newPage > 1 ? newPage : 1)
}, [])

useEffect(() => {
setData(current => {
if (pageData !== current[page]) {
return { ...current, [page]: pageData }
}

return current
})
}, [pageData, page])

useEffect(() => {
setPage(1)
setData({})
}, [baseFetchKey])

return {
data,
error,
goToNextPage,
goToPage,
goToPreviousPage,
isError,
isIdle,
isLoading,
isPolling,
isSuccess,
page,
pageData,
reload,
}
}

export default usePaginatedDataLoader