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
66 changes: 66 additions & 0 deletions packages/use-dataloader/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<React.StrictMode>
<DataLoaderProvider onError={globalOnError} cacheKeyPrefix="prefixed">
<App />
</DataLoaderProvider>
</React.StrictMode>,
document.getElementById('root'),
)
```

#### `onError(err: Error): void | Promise<void>`

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(
<React.StrictMode>
<DataLoaderProvider onError={globalOnError}>
<App />
</DataLoaderProvider>
</React.StrictMode>,
document.getElementById('root'),
)
```

### useDataLoader

```js
Expand Down
9 changes: 7 additions & 2 deletions packages/use-dataloader/src/DataLoaderProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ interface Context {
addCachedData: (key: string, newData: unknown) => void;
addReload: (key: string, method: () => Promise<void>) => void;
cacheKeyPrefix: string;
onError?: (error: Error) => void | Promise<void>
clearAllCachedData: () => void;
clearAllReloads: () => void;
clearCachedData: (key?: string | undefined) => void;
Expand All @@ -29,8 +30,8 @@ type Reloads = Record<string, () => Promise<void>>
// @ts-expect-error we force the context to undefined, should be corrected with default values
export const DataLoaderContext = createContext<Context>(undefined)

const DataLoaderProvider = ({ children, cacheKeyPrefix }: {
children: ReactNode, cacheKeyPrefix: string
const DataLoaderProvider = ({ children, cacheKeyPrefix, onError }: {
children: ReactNode, cacheKeyPrefix: string, onError: (error: Error) => void | Promise<void>
}): ReactElement => {
const cachedData = useRef<CachedData>({})
const reloads = useRef<Reloads>({})
Expand Down Expand Up @@ -149,6 +150,7 @@ const DataLoaderProvider = ({ children, cacheKeyPrefix }: {
clearReload,
getCachedData,
getReloads,
onError,
reload,
reloadAll,
}),
Expand All @@ -162,6 +164,7 @@ const DataLoaderProvider = ({ children, cacheKeyPrefix }: {
clearReload,
getCachedData,
getReloads,
onError,
reload,
reloadAll,
],
Expand All @@ -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)
Expand Down
81 changes: 79 additions & 2 deletions packages/use-dataloader/src/__tests__/useDataLoader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,18 @@ const initialProps = {
}),
}
// eslint-disable-next-line react/prop-types
const wrapper = ({ children }) => (
const wrapper = ({ children }: { children: React.ReactNode }) => (
<DataLoaderProvider>{children}</DataLoaderProvider>
)

const wrapperWithCacheKey = ({ children }) => (
const wrapperWithCacheKey = ({ children }: { children: React.ReactNode }) => (
<DataLoaderProvider cacheKeyPrefix="sample">{children}</DataLoaderProvider>
)

const wrapperWithOnError = (onError: (err: Error) => void) => ({ children }: { children: React.ReactNode }) => (
<DataLoaderProvider onError={onError}>{children}</DataLoaderProvider>
)

describe('useDataLoader', () => {
test('should render correctly without options', async () => {
const { result, waitForNextUpdate, rerender } = renderHook(
Expand Down Expand Up @@ -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)
})

Expand Down
10 changes: 6 additions & 4 deletions packages/use-dataloader/src/useDataLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -30,8 +30,8 @@ interface UseDataLoaderConfig<T> {
enabled?: boolean,
initialData?: T,
keepPreviousData?: boolean,
onError?: (err: Error) => Promise<void>,
onSuccess?: (data: T) => Promise<void>,
onError?: (err: Error) => void| Promise<void>,
onSuccess?: (data: T) => void | Promise<void>,
pollingInterval?: number,
}

Expand Down Expand Up @@ -83,6 +83,7 @@ const useDataLoader = <T>(
getCachedData,
addCachedData,
cacheKeyPrefix,
onError: onErrorProvider,
} = useDataLoaderContext()
const [{ status, error }, dispatch] = useReducer(reducer, {
error: undefined,
Expand Down Expand Up @@ -129,7 +130,7 @@ const useDataLoader = <T>(
await onSuccess?.(result)
} catch (err) {
dispatch(Actions.createOnError(err))
await onError?.(err)
await ((onError ?? onErrorProvider)?.(err))
}
},
[
Expand All @@ -138,6 +139,7 @@ const useDataLoader = <T>(
keepPreviousData,
method,
onError,
onErrorProvider,
onSuccess,
],
)
Expand Down