From 6014a679ed700519d220c0cff9ad282eb906cdb1 Mon Sep 17 00:00:00 2001 From: Emmanuel Chambon Date: Wed, 21 Jul 2021 10:56:06 +0200 Subject: [PATCH 1/2] feat(use-dataloader): add global onError handler --- .../use-dataloader/src/DataLoaderProvider.tsx | 9 ++- .../src/__tests__/useDataLoader.tsx | 81 ++++++++++++++++++- packages/use-dataloader/src/useDataLoader.ts | 10 ++- 3 files changed, 92 insertions(+), 8 deletions(-) diff --git a/packages/use-dataloader/src/DataLoaderProvider.tsx b/packages/use-dataloader/src/DataLoaderProvider.tsx index 36b13e911..d27c807ad 100644 --- a/packages/use-dataloader/src/DataLoaderProvider.tsx +++ b/packages/use-dataloader/src/DataLoaderProvider.tsx @@ -13,6 +13,7 @@ interface Context { addCachedData: (key: string, newData: unknown) => void; addReload: (key: string, method: () => Promise) => void; cacheKeyPrefix: string; + onError?: (error: Error) => void | Promise clearAllCachedData: () => void; clearAllReloads: () => void; clearCachedData: (key?: string | undefined) => void; @@ -29,8 +30,8 @@ type Reloads = Record Promise> // @ts-expect-error we force the context to undefined, should be corrected with default values export const DataLoaderContext = createContext(undefined) -const DataLoaderProvider = ({ children, cacheKeyPrefix }: { - children: ReactNode, cacheKeyPrefix: string +const DataLoaderProvider = ({ children, cacheKeyPrefix, onError }: { + children: ReactNode, cacheKeyPrefix: string, onError: (error: Error) => void | Promise }): ReactElement => { const cachedData = useRef({}) const reloads = useRef({}) @@ -149,6 +150,7 @@ const DataLoaderProvider = ({ children, cacheKeyPrefix }: { clearReload, getCachedData, getReloads, + onError, reload, reloadAll, }), @@ -162,6 +164,7 @@ const DataLoaderProvider = ({ children, cacheKeyPrefix }: { clearReload, getCachedData, getReloads, + onError, reload, reloadAll, ], @@ -177,10 +180,12 @@ const DataLoaderProvider = ({ children, cacheKeyPrefix }: { DataLoaderProvider.propTypes = { cacheKeyPrefix: PropTypes.string, children: PropTypes.node.isRequired, + onError: PropTypes.func, } DataLoaderProvider.defaultProps = { cacheKeyPrefix: undefined, + onError: undefined, } export const useDataLoaderContext = (): Context => useContext(DataLoaderContext) diff --git a/packages/use-dataloader/src/__tests__/useDataLoader.tsx b/packages/use-dataloader/src/__tests__/useDataLoader.tsx index 9f8f801c5..e0aa069ae 100644 --- a/packages/use-dataloader/src/__tests__/useDataLoader.tsx +++ b/packages/use-dataloader/src/__tests__/useDataLoader.tsx @@ -15,14 +15,18 @@ const initialProps = { }), } // eslint-disable-next-line react/prop-types -const wrapper = ({ children }) => ( +const wrapper = ({ children }: { children: React.ReactNode }) => ( {children} ) -const wrapperWithCacheKey = ({ children }) => ( +const wrapperWithCacheKey = ({ children }: { children: React.ReactNode }) => ( {children} ) +const wrapperWithOnError = (onError: (err: Error) => void) => ({ children }: { children: React.ReactNode }) => ( + {children} +) + describe('useDataLoader', () => { test('should render correctly without options', async () => { const { result, waitForNextUpdate, rerender } = renderHook( @@ -370,6 +374,79 @@ describe('useDataLoader', () => { expect(result.current.isError).toBe(true) expect(onError).toBeCalledTimes(1) + expect(onError).toBeCalledWith(error) + expect(onSuccess).toBeCalledTimes(0) + }) + + test('should override onError from Provider', async () => { + const onSuccess = jest.fn() + const onError = jest.fn() + const error = new Error('Test error') + const onErrorProvider = jest.fn() + const { result, waitForNextUpdate } = renderHook( + props => useDataLoader(props.key, props.method, props.config), + { + initialProps: { + config: { + onError, + onSuccess, + }, + key: 'test', + method: () => + new Promise((resolve, reject) => { + setTimeout(() => { + reject(error) + }, 500) + }), + }, + wrapper: wrapperWithOnError(onErrorProvider), + }, + ) + expect(result.current.data).toBe(undefined) + expect(result.current.isLoading).toBe(true) + await waitForNextUpdate() + expect(result.current.data).toBe(undefined) + expect(result.current.error).toBe(error) + expect(result.current.isError).toBe(true) + + expect(onError).toBeCalledTimes(1) + expect(onError).toBeCalledWith(error) + expect(onErrorProvider).toBeCalledTimes(0) + expect(onSuccess).toBeCalledTimes(0) + }) + + test('should call onError from Provider', async () => { + const onSuccess = jest.fn() + const error = new Error('Test error') + const onErrorProvider = jest.fn() + const { result, waitForNextUpdate } = renderHook( + props => useDataLoader(props.key, props.method, props.config), + { + initialProps: { + config: { + onSuccess, + }, + key: 'test', + method: () => + new Promise((resolve, reject) => { + setTimeout(() => { + reject(error) + }, 500) + }), + }, + wrapper: wrapperWithOnError(onErrorProvider), + }, + ) + + expect(result.current.data).toBe(undefined) + expect(result.current.isLoading).toBe(true) + await waitForNextUpdate() + expect(result.current.data).toBe(undefined) + expect(result.current.error).toBe(error) + expect(result.current.isError).toBe(true) + + expect(onErrorProvider).toBeCalledTimes(1) + expect(onErrorProvider).toBeCalledWith(error) expect(onSuccess).toBeCalledTimes(0) }) diff --git a/packages/use-dataloader/src/useDataLoader.ts b/packages/use-dataloader/src/useDataLoader.ts index 4da22f66a..c16a4c771 100644 --- a/packages/use-dataloader/src/useDataLoader.ts +++ b/packages/use-dataloader/src/useDataLoader.ts @@ -20,7 +20,7 @@ const Actions = { /** * @typedef {Object} UseDataLoaderConfig * @property {Function} [onSuccess] callback when a request success - * @property {Function} [onError] callback when a error is occured + * @property {Function} [onError] callback when a error is occured, this will override the onError specified on the Provider if any * @property {*} [initialData] initial data if no one is present in the cache before the request * @property {number} [pollingInterval] relaunch the request after the last success * @property {boolean} [enabled=true] launch request automatically (default true) @@ -30,8 +30,8 @@ interface UseDataLoaderConfig { enabled?: boolean, initialData?: T, keepPreviousData?: boolean, - onError?: (err: Error) => Promise, - onSuccess?: (data: T) => Promise, + onError?: (err: Error) => void| Promise, + onSuccess?: (data: T) => void | Promise, pollingInterval?: number, } @@ -83,6 +83,7 @@ const useDataLoader = ( getCachedData, addCachedData, cacheKeyPrefix, + onError: onErrorProvider, } = useDataLoaderContext() const [{ status, error }, dispatch] = useReducer(reducer, { error: undefined, @@ -129,7 +130,7 @@ const useDataLoader = ( await onSuccess?.(result) } catch (err) { dispatch(Actions.createOnError(err)) - await onError?.(err) + await ((onError ?? onErrorProvider)?.(err)) } }, [ @@ -138,6 +139,7 @@ const useDataLoader = ( keepPreviousData, method, onError, + onErrorProvider, onSuccess, ], ) From a8ce4c86675d0d6f06b63c1417bfbb0e0992ad71 Mon Sep 17 00:00:00 2001 From: Emmanuel Chambon Date: Wed, 21 Jul 2021 11:09:01 +0200 Subject: [PATCH 2/2] docs: update README --- packages/use-dataloader/README.md | 66 +++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/packages/use-dataloader/README.md b/packages/use-dataloader/README.md index fca49a960..fd126030d 100644 --- a/packages/use-dataloader/README.md +++ b/packages/use-dataloader/README.md @@ -36,6 +36,72 @@ ReactDOM.render( Now you can use `useDataLoader` and `useDataLoaderContext` in your App +#### `cacheKeyPrefix` + +You can specify a global `cacheKeyPrefix` which will be inserted before each cache key + +This can be useful if you have a global context (eg: if you can switch account in your app, ...) + +```js +import { DataLoaderProvider, useDataLoader } from '@scaleway-lib/use-dataloader' +import React from 'react' +import ReactDOM from 'react-dom' + +const App = () => { + useDataLoader('cache-key', () => 'response') // Real key will be prefixed-cache-key + + return null +} + +ReactDOM.render( + + + + + , + document.getElementById('root'), +) +``` + +#### `onError(err: Error): void | Promise` + +This is a global `onError` handler. It will be overriden if you specify one in `useDataLoader` + +```js +import { DataLoaderProvider, useDataLoader } from '@scaleway-lib/use-dataloader' +import React from 'react' +import ReactDOM from 'react-dom' + +const failingPromise = async () => { + throw new Error('error') +} + +const App = () => { + useDataLoader('local-error', failingPromise, { + onError: (error) => { + console.log(`local onError: ${error}`) + } + }) + + useDataLoader('error', failingPromise) + + return null +} + +const globalOnError = (error) => { + console.log(`global onError: ${error}`) +} + +ReactDOM.render( + + + + + , + document.getElementById('root'), +) +``` + ### useDataLoader ```js