diff --git a/packages/use-dataloader/README.md b/packages/use-dataloader/README.md index d97dc54d0..f0311e21f 100644 --- a/packages/use-dataloader/README.md +++ b/packages/use-dataloader/README.md @@ -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) | diff --git a/packages/use-dataloader/src/__tests__/useDataLoader.test.tsx b/packages/use-dataloader/src/__tests__/useDataLoader.test.tsx index 927163d35..00f90d090 100644 --- a/packages/use-dataloader/src/__tests__/useDataLoader.test.tsx +++ b/packages/use-dataloader/src/__tests__/useDataLoader.test.tsx @@ -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 }) => ( {children} @@ -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 */ diff --git a/packages/use-dataloader/src/dataloader.ts b/packages/use-dataloader/src/dataloader.ts index 113971b8e..4dad0814e 100644 --- a/packages/use-dataloader/src/dataloader.ts +++ b/packages/use-dataloader/src/dataloader.ts @@ -39,6 +39,8 @@ class DataLoader { public isFirstLoading = true + public dataUpdatedAt?: number + public constructor(args: DataLoaderConstructorArgs) { this.key = args.key this.method = args.method @@ -102,6 +104,7 @@ class DataLoader { this.status = StatusEnum.SUCCESS this.data = data this.error = undefined + this.dataUpdatedAt = Date.now() } this.isCalled = false this.isFirstLoading = false diff --git a/packages/use-dataloader/src/types.ts b/packages/use-dataloader/src/types.ts index 1655ac84a..e03a3d984 100644 --- a/packages/use-dataloader/src/types.ts +++ b/packages/use-dataloader/src/types.ts @@ -17,6 +17,7 @@ export type NeedPollingType = 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 { @@ -26,10 +27,7 @@ export interface UseDataLoaderConfig { 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 } diff --git a/packages/use-dataloader/src/useDataLoader.ts b/packages/use-dataloader/src/useDataLoader.ts index 545cf810f..7c903d463 100644 --- a/packages/use-dataloader/src/useDataLoader.ts +++ b/packages/use-dataloader/src/useDataLoader.ts @@ -15,6 +15,7 @@ function useDataLoader( needPolling = true, pollingInterval, initialData, + dataLifetime, }: UseDataLoaderConfig = {}, ): UseDataLoaderResult { const { getOrAddRequest, onError: onGlobalError } = useDataLoaderContext() @@ -86,10 +87,18 @@ function useDataLoader( }, [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 diff --git a/packages/use-dataloader/src/usePaginatedDataLoader.ts b/packages/use-dataloader/src/usePaginatedDataLoader.ts index 4370bcb56..a2df1be76 100644 --- a/packages/use-dataloader/src/usePaginatedDataLoader.ts +++ b/packages/use-dataloader/src/usePaginatedDataLoader.ts @@ -24,7 +24,7 @@ const usePaginatedDataLoader = ( onError, onSuccess, pollingInterval, - maxDataLifetime, + dataLifetime, needPolling, initialPage, perPage = 1, @@ -51,10 +51,10 @@ const usePaginatedDataLoader = ( reload, error, } = useDataLoader(`${baseFetchKey}-page-${page}`, pageMethod, { + dataLifetime, enabled, initialData, keepPreviousData, - maxDataLifetime, needPolling, onError, onSuccess,