Skip to content

Commit e881b5e

Browse files
authored
feat(use-dataloader): add global onError handler (#286)
* feat(use-dataloader): add global onError handler * docs: update README
1 parent 7ef9832 commit e881b5e

File tree

4 files changed

+158
-8
lines changed

4 files changed

+158
-8
lines changed

packages/use-dataloader/README.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,72 @@ ReactDOM.render(
3636

3737
Now you can use `useDataLoader` and `useDataLoaderContext` in your App
3838

39+
#### `cacheKeyPrefix`
40+
41+
You can specify a global `cacheKeyPrefix` which will be inserted before each cache key
42+
43+
This can be useful if you have a global context (eg: if you can switch account in your app, ...)
44+
45+
```js
46+
import { DataLoaderProvider, useDataLoader } from '@scaleway-lib/use-dataloader'
47+
import React from 'react'
48+
import ReactDOM from 'react-dom'
49+
50+
const App = () => {
51+
useDataLoader('cache-key', () => 'response') // Real key will be prefixed-cache-key
52+
53+
return null
54+
}
55+
56+
ReactDOM.render(
57+
<React.StrictMode>
58+
<DataLoaderProvider onError={globalOnError} cacheKeyPrefix="prefixed">
59+
<App />
60+
</DataLoaderProvider>
61+
</React.StrictMode>,
62+
document.getElementById('root'),
63+
)
64+
```
65+
66+
#### `onError(err: Error): void | Promise<void>`
67+
68+
This is a global `onError` handler. It will be overriden if you specify one in `useDataLoader`
69+
70+
```js
71+
import { DataLoaderProvider, useDataLoader } from '@scaleway-lib/use-dataloader'
72+
import React from 'react'
73+
import ReactDOM from 'react-dom'
74+
75+
const failingPromise = async () => {
76+
throw new Error('error')
77+
}
78+
79+
const App = () => {
80+
useDataLoader('local-error', failingPromise, {
81+
onError: (error) => {
82+
console.log(`local onError: ${error}`)
83+
}
84+
})
85+
86+
useDataLoader('error', failingPromise)
87+
88+
return null
89+
}
90+
91+
const globalOnError = (error) => {
92+
console.log(`global onError: ${error}`)
93+
}
94+
95+
ReactDOM.render(
96+
<React.StrictMode>
97+
<DataLoaderProvider onError={globalOnError}>
98+
<App />
99+
</DataLoaderProvider>
100+
</React.StrictMode>,
101+
document.getElementById('root'),
102+
)
103+
```
104+
39105
### useDataLoader
40106

41107
```js

packages/use-dataloader/src/DataLoaderProvider.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ interface Context {
1313
addCachedData: (key: string, newData: unknown) => void;
1414
addReload: (key: string, method: () => Promise<void>) => void;
1515
cacheKeyPrefix: string;
16+
onError?: (error: Error) => void | Promise<void>
1617
clearAllCachedData: () => void;
1718
clearAllReloads: () => void;
1819
clearCachedData: (key?: string | undefined) => void;
@@ -29,8 +30,8 @@ type Reloads = Record<string, () => Promise<void>>
2930
// @ts-expect-error we force the context to undefined, should be corrected with default values
3031
export const DataLoaderContext = createContext<Context>(undefined)
3132

32-
const DataLoaderProvider = ({ children, cacheKeyPrefix }: {
33-
children: ReactNode, cacheKeyPrefix: string
33+
const DataLoaderProvider = ({ children, cacheKeyPrefix, onError }: {
34+
children: ReactNode, cacheKeyPrefix: string, onError: (error: Error) => void | Promise<void>
3435
}): ReactElement => {
3536
const cachedData = useRef<CachedData>({})
3637
const reloads = useRef<Reloads>({})
@@ -149,6 +150,7 @@ const DataLoaderProvider = ({ children, cacheKeyPrefix }: {
149150
clearReload,
150151
getCachedData,
151152
getReloads,
153+
onError,
152154
reload,
153155
reloadAll,
154156
}),
@@ -162,6 +164,7 @@ const DataLoaderProvider = ({ children, cacheKeyPrefix }: {
162164
clearReload,
163165
getCachedData,
164166
getReloads,
167+
onError,
165168
reload,
166169
reloadAll,
167170
],
@@ -177,10 +180,12 @@ const DataLoaderProvider = ({ children, cacheKeyPrefix }: {
177180
DataLoaderProvider.propTypes = {
178181
cacheKeyPrefix: PropTypes.string,
179182
children: PropTypes.node.isRequired,
183+
onError: PropTypes.func,
180184
}
181185

182186
DataLoaderProvider.defaultProps = {
183187
cacheKeyPrefix: undefined,
188+
onError: undefined,
184189
}
185190

186191
export const useDataLoaderContext = (): Context => useContext(DataLoaderContext)

packages/use-dataloader/src/__tests__/useDataLoader.tsx

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,18 @@ const initialProps = {
1515
}),
1616
}
1717
// eslint-disable-next-line react/prop-types
18-
const wrapper = ({ children }) => (
18+
const wrapper = ({ children }: { children: React.ReactNode }) => (
1919
<DataLoaderProvider>{children}</DataLoaderProvider>
2020
)
2121

22-
const wrapperWithCacheKey = ({ children }) => (
22+
const wrapperWithCacheKey = ({ children }: { children: React.ReactNode }) => (
2323
<DataLoaderProvider cacheKeyPrefix="sample">{children}</DataLoaderProvider>
2424
)
2525

26+
const wrapperWithOnError = (onError: (err: Error) => void) => ({ children }: { children: React.ReactNode }) => (
27+
<DataLoaderProvider onError={onError}>{children}</DataLoaderProvider>
28+
)
29+
2630
describe('useDataLoader', () => {
2731
test('should render correctly without options', async () => {
2832
const { result, waitForNextUpdate, rerender } = renderHook(
@@ -370,6 +374,79 @@ describe('useDataLoader', () => {
370374
expect(result.current.isError).toBe(true)
371375

372376
expect(onError).toBeCalledTimes(1)
377+
expect(onError).toBeCalledWith(error)
378+
expect(onSuccess).toBeCalledTimes(0)
379+
})
380+
381+
test('should override onError from Provider', async () => {
382+
const onSuccess = jest.fn()
383+
const onError = jest.fn()
384+
const error = new Error('Test error')
385+
const onErrorProvider = jest.fn()
386+
const { result, waitForNextUpdate } = renderHook(
387+
props => useDataLoader(props.key, props.method, props.config),
388+
{
389+
initialProps: {
390+
config: {
391+
onError,
392+
onSuccess,
393+
},
394+
key: 'test',
395+
method: () =>
396+
new Promise((resolve, reject) => {
397+
setTimeout(() => {
398+
reject(error)
399+
}, 500)
400+
}),
401+
},
402+
wrapper: wrapperWithOnError(onErrorProvider),
403+
},
404+
)
405+
expect(result.current.data).toBe(undefined)
406+
expect(result.current.isLoading).toBe(true)
407+
await waitForNextUpdate()
408+
expect(result.current.data).toBe(undefined)
409+
expect(result.current.error).toBe(error)
410+
expect(result.current.isError).toBe(true)
411+
412+
expect(onError).toBeCalledTimes(1)
413+
expect(onError).toBeCalledWith(error)
414+
expect(onErrorProvider).toBeCalledTimes(0)
415+
expect(onSuccess).toBeCalledTimes(0)
416+
})
417+
418+
test('should call onError from Provider', async () => {
419+
const onSuccess = jest.fn()
420+
const error = new Error('Test error')
421+
const onErrorProvider = jest.fn()
422+
const { result, waitForNextUpdate } = renderHook(
423+
props => useDataLoader(props.key, props.method, props.config),
424+
{
425+
initialProps: {
426+
config: {
427+
onSuccess,
428+
},
429+
key: 'test',
430+
method: () =>
431+
new Promise((resolve, reject) => {
432+
setTimeout(() => {
433+
reject(error)
434+
}, 500)
435+
}),
436+
},
437+
wrapper: wrapperWithOnError(onErrorProvider),
438+
},
439+
)
440+
441+
expect(result.current.data).toBe(undefined)
442+
expect(result.current.isLoading).toBe(true)
443+
await waitForNextUpdate()
444+
expect(result.current.data).toBe(undefined)
445+
expect(result.current.error).toBe(error)
446+
expect(result.current.isError).toBe(true)
447+
448+
expect(onErrorProvider).toBeCalledTimes(1)
449+
expect(onErrorProvider).toBeCalledWith(error)
373450
expect(onSuccess).toBeCalledTimes(0)
374451
})
375452

packages/use-dataloader/src/useDataLoader.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ const Actions = {
2020
/**
2121
* @typedef {Object} UseDataLoaderConfig
2222
* @property {Function} [onSuccess] callback when a request success
23-
* @property {Function} [onError] callback when a error is occured
23+
* @property {Function} [onError] callback when a error is occured, this will override the onError specified on the Provider if any
2424
* @property {*} [initialData] initial data if no one is present in the cache before the request
2525
* @property {number} [pollingInterval] relaunch the request after the last success
2626
* @property {boolean} [enabled=true] launch request automatically (default true)
@@ -30,8 +30,8 @@ interface UseDataLoaderConfig<T> {
3030
enabled?: boolean,
3131
initialData?: T,
3232
keepPreviousData?: boolean,
33-
onError?: (err: Error) => Promise<void>,
34-
onSuccess?: (data: T) => Promise<void>,
33+
onError?: (err: Error) => void| Promise<void>,
34+
onSuccess?: (data: T) => void | Promise<void>,
3535
pollingInterval?: number,
3636
}
3737

@@ -83,6 +83,7 @@ const useDataLoader = <T>(
8383
getCachedData,
8484
addCachedData,
8585
cacheKeyPrefix,
86+
onError: onErrorProvider,
8687
} = useDataLoaderContext()
8788
const [{ status, error }, dispatch] = useReducer(reducer, {
8889
error: undefined,
@@ -129,7 +130,7 @@ const useDataLoader = <T>(
129130
await onSuccess?.(result)
130131
} catch (err) {
131132
dispatch(Actions.createOnError(err))
132-
await onError?.(err)
133+
await ((onError ?? onErrorProvider)?.(err))
133134
}
134135
},
135136
[
@@ -138,6 +139,7 @@ const useDataLoader = <T>(
138139
keepPreviousData,
139140
method,
140141
onError,
142+
onErrorProvider,
141143
onSuccess,
142144
],
143145
)

0 commit comments

Comments
 (0)