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
25 changes: 13 additions & 12 deletions packages/use-dataloader/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,21 +228,22 @@ const useDataLoader = (
onError, // Callback when a error is occured
initialData, // Initial data if no one is present in the cache before the request
pollingInterval, // Relaunch the request after the last success
needPolling = true, // If true or function return true it will execute the polling
enabled = true, // Launch request automatically
keepPreviousData = true, // Do we need to keep the previous data after reload
maxDataLifetime, // Max time before previous success data is outdated (in millisecond)
dataLifetime, // Max time before previous success data is outdated (in millisecond). By default refetch on every mount
} = {},
)
```

| Property | Description |
| :----------: | :-------------------------------------------------------------------------------------------------------------------: |
| isIdle | `true` if the request is not launched |
| isLoading | `true` if the request is launched **or** enabled is `true` and isIdle is `true` |
| isSuccess | `true`if the request finished successfully |
| isError | `true` if the request throw an error |
| isPolling | `true` if the request if `enabled` is true, `pollingInterval` is defined and the status is `isLoading` or `isSuccess` |
| previousData | if `keepPreviousData` is true it return the last data fetched |
| data | return the `initialData` if no data is fetched or not present in the cache otherwise return the data fetched |
| error | return the error occured during the request |
| reload | allow you to reload the data (it doesn't clear the actual data) |
| Property | Description |
| :----------: | :------------------------------------------------------------------------------------------------------------------------------------------: |
| isIdle | `true` if the request is not launched |
| isLoading | `true` if the request is launched **or** enabled is `true` and isIdle is `true` |
| isSuccess | `true`if the request finished successfully |
| isError | `true` if the request throw an error |
| isPolling | `true` if the request if `enabled` is true, `pollingInterval` is defined and the status is `isLoading`,`isSuccess` or during the first fetch |
| previousData | if `keepPreviousData` is true it return the last data fetched |
| data | return the `initialData` if no data is fetched or not present in the cache otherwise return the data fetched |
| error | return the error occured during the request |
| reload | allow you to reload the data (it doesn't clear the actual data) |
92 changes: 86 additions & 6 deletions packages/use-dataloader/src/__tests__/useDataLoader.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,18 @@ type UseDataLoaderHookProps = {

const PROMISE_TIMEOUT = 50

const fakeSuccessPromise = () =>
new Promise(resolve => {
setTimeout(() => resolve(true), PROMISE_TIMEOUT)
})

const initialProps = {
config: {
enabled: true,
keepPreviousData: true,
},
key: 'test',
method: jest.fn(
() =>
new Promise(resolve => {
setTimeout(() => resolve(true), PROMISE_TIMEOUT)
}),
),
method: jest.fn(fakeSuccessPromise),
}
const wrapper = ({ children }: { children?: ReactNode }) => (
<DataLoaderProvider>{children}</DataLoaderProvider>
Expand Down Expand Up @@ -735,5 +735,85 @@ describe('useDataLoader', () => {
await waitFor(() => expect(result.current[0].isSuccess).toBe(true))
expect(mockedFn).toBeCalledTimes(2)
})

test('should render correctly with dataLifetime prevent double call', async () => {
const testingProps = {
config: {
dataLifetime: 1000,
enabled: true,
},
config2: {
dataLifetime: 1000,
enabled: false,
},
key: 'test-datalifetime',
method: jest.fn(fakeSuccessPromise),
}
const { result, rerender } = renderHook(
props => [
useDataLoader(props.key, props.method, props.config),
useDataLoader(props.key, props.method, props.config2),
],
{
initialProps: testingProps,
wrapper,
},
)
expect(result.current[0].data).toBe(undefined)
expect(result.current[0].isLoading).toBe(true)
expect(result.current[0].previousData).toBe(undefined)
expect(testingProps.method).toBeCalledTimes(1)
await waitFor(() => expect(result.current[0].isSuccess).toBe(true))
testingProps.config2.enabled = true
rerender(testingProps)
expect(testingProps.method).toBeCalledTimes(1)
expect(result.current[0].data).toBe(true)
expect(result.current[1].data).toBe(true)
expect(result.current[0].isLoading).toBe(false)
expect(result.current[1].isLoading).toBe(false)
expect(result.current[0].previousData).toBe(undefined)
expect(result.current[1].previousData).toBe(undefined)
})

test('should render correctly with dataLifetime dont prevent double call', async () => {
const testingProps = {
config: {
enabled: true,
},
config2: {
enabled: false,
},
key: 'test-no-datalifetime',
method: jest.fn(fakeSuccessPromise),
}
const { result, rerender } = renderHook(
props => [
useDataLoader(props.key, props.method, props.config),
useDataLoader(props.key, props.method, props.config2),
],
{
initialProps: testingProps,
wrapper,
},
)
expect(result.current[0].data).toBe(undefined)
expect(result.current[0].isLoading).toBe(true)
expect(result.current[0].previousData).toBe(undefined)
expect(testingProps.method).toBeCalledTimes(1)
await waitFor(() => expect(result.current[0].isSuccess).toBe(true))
testingProps.config2.enabled = true
rerender(testingProps)
await waitFor(() => expect(result.current[0].isLoading).toBe(true))
await waitFor(() => expect(result.current[1].isLoading).toBe(true))
expect(testingProps.method).toBeCalledTimes(2)
expect(result.current[0].data).toBe(true)
expect(result.current[0].isLoading).toBe(true)
expect(result.current[0].previousData).toBe(undefined)
expect(result.current[1].data).toBe(true)
expect(result.current[1].isLoading).toBe(true)
expect(result.current[1].previousData).toBe(undefined)
await waitFor(() => expect(result.current[0].isSuccess).toBe(true))
await waitFor(() => expect(result.current[1].isSuccess).toBe(true))
})
})
/* eslint-enable no-console */
3 changes: 3 additions & 0 deletions packages/use-dataloader/src/dataloader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ class DataLoader<ResultType = unknown, ErrorType = unknown> {

public isFirstLoading = true

public dataUpdatedAt?: number

public constructor(args: DataLoaderConstructorArgs<ResultType>) {
this.key = args.key
this.method = args.method
Expand Down Expand Up @@ -102,6 +104,7 @@ class DataLoader<ResultType = unknown, ErrorType = unknown> {
this.status = StatusEnum.SUCCESS
this.data = data
this.error = undefined
this.dataUpdatedAt = Date.now()
}
this.isCalled = false
this.isFirstLoading = false
Expand Down
6 changes: 2 additions & 4 deletions packages/use-dataloader/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export type NeedPollingType<T = unknown> = boolean | ((data?: T) => boolean)
* @property {number} [pollingInterval] relaunch the request after the last success
* @property {boolean} [enabled=true] launch request automatically (default true)
* @property {boolean} [keepPreviousData=true] do we need to keep the previous data after reload (default true)
* @property {number} [dataLifetime=undefined] Time before data from previous success is considered as outdated (in millisecond)
* @property {NeedPollingType} [needPolling=true] When pollingInterval is set you can set a set a custom callback to know if polling is enabled
*/
export interface UseDataLoaderConfig<T = unknown> {
Expand All @@ -26,10 +27,7 @@ export interface UseDataLoaderConfig<T = unknown> {
onError?: OnErrorFn
onSuccess?: OnSuccessFn
pollingInterval?: number
/**
* Max time before data from previous success is considered as outdated (in millisecond)
*/
maxDataLifetime?: number
dataLifetime?: number
needPolling?: NeedPollingType<T>
}

Expand Down
13 changes: 11 additions & 2 deletions packages/use-dataloader/src/useDataLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ function useDataLoader<ResultType, ErrorType = Error>(
needPolling = true,
pollingInterval,
initialData,
dataLifetime,
}: UseDataLoaderConfig<ResultType> = {},
): UseDataLoaderResult<ResultType, ErrorType> {
const { getOrAddRequest, onError: onGlobalError } = useDataLoaderContext()
Expand Down Expand Up @@ -86,10 +87,18 @@ function useDataLoader<ResultType, ErrorType = Error>(
}, [request.data, keepPreviousData])

useEffect(() => {
if (enabled && !request.isCalled) {
// If this request is enabled and not already called
if (
enabled &&
(!request.dataUpdatedAt ||
!dataLifetime ||
(request.dataUpdatedAt &&
dataLifetime &&
request.dataUpdatedAt + dataLifetime < Date.now()))
) {
request.load().then(onSuccessRef.current).catch(onErrorRef.current)
}
}, [enabled, request, keepPreviousData])
}, [enabled, request, keepPreviousData, dataLifetime])

useEffect(() => {
let interval: NodeJS.Timer
Expand Down
4 changes: 2 additions & 2 deletions packages/use-dataloader/src/usePaginatedDataLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const usePaginatedDataLoader = <T>(
onError,
onSuccess,
pollingInterval,
maxDataLifetime,
dataLifetime,
needPolling,
initialPage,
perPage = 1,
Expand All @@ -51,10 +51,10 @@ const usePaginatedDataLoader = <T>(
reload,
error,
} = useDataLoader(`${baseFetchKey}-page-${page}`, pageMethod, {
dataLifetime,
enabled,
initialData,
keepPreviousData,
maxDataLifetime,
needPolling,
onError,
onSuccess,
Expand Down