Skip to content

Commit acd938f

Browse files
fix: DataLoaderProvider handle data return from useDataLoader (#150)
1 parent 2debbb0 commit acd938f

File tree

4 files changed

+139
-66
lines changed

4 files changed

+139
-66
lines changed

packages/use-dataloader/README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,3 +135,36 @@ function MyComponent() {
135135

136136
export default MyComponent
137137
```
138+
139+
---
140+
141+
## API
142+
143+
### useDataLoader
144+
145+
```js
146+
const useDataLoader = (
147+
key, // A key to save the data fetched in a local cache
148+
method, // A method that return a promise (ex: () => new Promise((resolve) => setTimeout(resolve, 2000))
149+
{
150+
onSuccess, // Callback when a request success
151+
onError, // Callback when a error is occured
152+
initialData, // Initial data if no one is present in the cache before the request
153+
pollingInterval, // Relaunch the request after the last success
154+
enabled = true, // Launch request automatically
155+
keepPreviousData = true, // Do we need to keep the previous data after reload
156+
} = {},
157+
)
158+
```
159+
160+
| Property | Description |
161+
| :----------: | :-------------------------------------------------------------------------------------------------------------------: |
162+
| isIdle | `true` if the request is not launched |
163+
| isLoading | `true` if the request is launched |
164+
| isSuccess | `true`if the request finished successfully |
165+
| isError | `true` if the request throw an error |
166+
| isPolling | `true` if the request if `enabled` is true, `pollingInterval` is defined and the status is `isLoading` or `isSuccess` |
167+
| previousData | if `keepPreviousData` is true it return the last data fetched |
168+
| data | return the `initialData` if no data is fetched or not present in the cache otherwise return the data fetched |
169+
| error | return the error occured during the request |
170+
| reload | allow you to reload the data (it doesn't clear the actual data) |

packages/use-dataloader/src/__tests__/useDataLoader.test.js

Lines changed: 52 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const initialProps = {
1111
}),
1212
config: {
1313
enabled: true,
14-
reloadOnKeyChange: false,
14+
keepPreviousData: true,
1515
},
1616
}
1717
// eslint-disable-next-line react/prop-types
@@ -37,40 +37,69 @@ describe('useDataLoader', () => {
3737
expect(result.current.isLoading).toBe(false)
3838
})
3939

40-
test('should render correctly with enabled true', async () => {
41-
const { result, waitForNextUpdate, rerender } = renderHook(
40+
test('should render correctly without valid key', async () => {
41+
const { result, waitForNextUpdate } = renderHook(
42+
props => useDataLoader(props.key, props.method),
43+
{
44+
wrapper,
45+
initialProps: {
46+
...initialProps,
47+
key: 2,
48+
},
49+
},
50+
)
51+
expect(result.current.data).toBe(undefined)
52+
expect(result.current.isLoading).toBe(true)
53+
await waitForNextUpdate()
54+
expect(result.current.data).toBe(undefined)
55+
expect(result.current.isSuccess).toBe(true)
56+
expect(result.current.isLoading).toBe(false)
57+
})
58+
59+
test('should render correctly without keepPreviousData', async () => {
60+
const { result, waitForNextUpdate } = renderHook(
4261
props => useDataLoader(props.key, props.method, props.config),
4362
{
4463
wrapper,
45-
initialProps,
64+
initialProps: {
65+
...initialProps,
66+
config: {
67+
keepPreviousData: false,
68+
},
69+
},
4670
},
4771
)
4872
expect(result.current.data).toBe(undefined)
4973
expect(result.current.isLoading).toBe(true)
50-
rerender()
5174
await waitForNextUpdate()
5275
expect(result.current.data).toBe(true)
5376
expect(result.current.isSuccess).toBe(true)
5477
expect(result.current.isLoading).toBe(false)
78+
})
5579

56-
act(() => {
57-
result.current.reload()
58-
})
59-
act(() => {
60-
result.current.reload()
61-
})
62-
63-
expect(result.current.data).toBe(true)
80+
test('should render correctly with result null', async () => {
81+
const { result, waitForNextUpdate } = renderHook(
82+
props => useDataLoader(props.key, props.method, props.config),
83+
{
84+
wrapper,
85+
initialProps: {
86+
...initialProps,
87+
method: () =>
88+
new Promise(resolve => setTimeout(() => resolve(null), 100)),
89+
},
90+
},
91+
)
92+
expect(result.current.data).toBe(undefined)
6493
expect(result.current.isLoading).toBe(true)
6594
await waitForNextUpdate()
66-
expect(result.current.data).toBe(true)
95+
expect(result.current.data).toBe(undefined)
6796
expect(result.current.isSuccess).toBe(true)
6897
expect(result.current.isLoading).toBe(false)
6998
})
7099

71-
test('should render correctly with bad key', async () => {
100+
test('should render correctly with enabled true', async () => {
72101
const { result, waitForNextUpdate } = renderHook(
73-
props => useDataLoader(undefined, props.method, props.config),
102+
props => useDataLoader(props.key, props.method, props.config),
74103
{
75104
wrapper,
76105
initialProps,
@@ -99,35 +128,33 @@ describe('useDataLoader', () => {
99128
})
100129

101130
test('should render correctly with key update', async () => {
102-
let key = 'test'
103131
const propsToPass = {
104132
...initialProps,
105-
key,
133+
key: 'test',
106134
config: {
107135
reloadOnKeyChange: true,
108136
},
109137
}
110138
const { result, waitForNextUpdate, rerender } = renderHook(
111-
props => useDataLoader(key, props.method, props.config),
139+
() =>
140+
useDataLoader(propsToPass.key, propsToPass.method, propsToPass.config),
112141
{
113142
wrapper,
114-
initialProps: propsToPass,
115143
},
116144
)
117145

118146
expect(result.current.data).toBe(undefined)
119147
expect(result.current.isLoading).toBe(true)
120148
await waitForNextUpdate()
121-
expect(result.current.data).toBe(true)
122149
expect(result.current.isSuccess).toBe(true)
123150
expect(result.current.isLoading).toBe(false)
151+
expect(result.current.data).toBe(true)
124152

125-
key = 'new-test'
153+
propsToPass.key = 'key-2'
126154
rerender()
127-
128-
expect(result.current.data).toBe(undefined)
129155
expect(result.current.isLoading).toBe(true)
130-
key = 'new-new-test'
156+
expect(result.current.data).toBe(undefined)
157+
propsToPass.key = 'new-new-test'
131158
rerender()
132159
expect(result.current.data).toBe(undefined)
133160
expect(result.current.isLoading).toBe(true)

packages/use-dataloader/src/reducer.js

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,11 @@ export default (state, action) => {
1414
return {
1515
...state,
1616
error: undefined,
17-
data: action.data,
1817
status: StatusEnum.SUCCESS,
1918
}
20-
case ActionEnum.ON_UPDATE_DATA:
21-
return {
22-
...state,
23-
data: action.data,
24-
}
2519
case ActionEnum.RESET:
2620
return {
2721
status: StatusEnum.IDLE,
28-
data: action.data,
2922
error: undefined,
3023
}
3124
case ActionEnum.ON_ERROR:

packages/use-dataloader/src/useDataLoader.js

Lines changed: 54 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,41 @@ import { ActionEnum, StatusEnum } from './constants'
44
import reducer from './reducer'
55

66
const Actions = {
7-
createReset: ({ data }) => ({ type: ActionEnum.RESET, data }),
7+
createReset: () => ({ type: ActionEnum.RESET }),
88
createOnLoading: () => ({ type: ActionEnum.ON_LOADING }),
9-
createOnSuccess: data => ({ type: ActionEnum.ON_SUCCESS, data }),
10-
createOnUpdateData: data => ({ type: ActionEnum.ON_UPDATE_DATA, data }),
9+
createOnSuccess: () => ({ type: ActionEnum.ON_SUCCESS }),
1110
createOnError: error => ({ type: ActionEnum.ON_ERROR, error }),
1211
}
1312

13+
/**
14+
* @typedef {Object} useDataLoaderConfig
15+
* @property {Function} [onSuccess] callback when a request success
16+
* @property {Function} [onError] callback when a error is occured
17+
* @property {*} [initialData] initial data if no one is present in the cache before the request
18+
* @property {number} [pollingInterval] relaunch the request after the last success
19+
* @property {boolean} [enabled=true] launch request automatically (default true)
20+
* @property {boolean} [keepPreviousData=true] do we need to keep the previous data after reload (default true)
21+
*/
22+
23+
/**
24+
* @typedef {Object} useDataLoaderResult
25+
* @property {boolean} isIdle true if the hook in initial state
26+
* @property {boolean} isLoading true if the request is launched
27+
* @property {boolean} isSuccess true if the request success
28+
* @property {boolean} isError true if the request throw an error
29+
* @property {boolean} isPolling true if the request if enabled is true, pollingInterval is defined and the status is isLoading or isSuccess
30+
* @property {*} previousData if keepPreviousData is true it return the last data fetched
31+
* @property {*} data initialData if no data is fetched or not present in the cache otherwise return the data fetched
32+
* @property {string} error the error occured during the request
33+
* @property {Function} reload reload the data
34+
*/
35+
36+
/**
37+
* @param {string} key key to save the data fetched in a local cache
38+
* @param {() => Promise} method a method that return a promise
39+
* @param {useDataLoaderConfig} config hook configuration
40+
* @returns {useDataLoaderResult} hook result containing data, request state, and method to reload the data
41+
*/
1442
const useDataLoader = (
1543
key,
1644
method,
@@ -20,7 +48,6 @@ const useDataLoader = (
2048
initialData,
2149
pollingInterval,
2250
enabled = true,
23-
reloadOnKeyChange = true,
2451
keepPreviousData = true,
2552
} = {},
2653
) => {
@@ -30,14 +57,13 @@ const useDataLoader = (
3057
clearReload,
3158
getCachedData,
3259
} = useDataLoaderContext()
33-
const [{ status, data, error }, dispatch] = useReducer(reducer, {
60+
const [{ status, error }, dispatch] = useReducer(reducer, {
3461
status: StatusEnum.IDLE,
35-
data: initialData,
3662
error: undefined,
3763
})
3864

3965
const previousDataRef = useRef()
40-
const keyRef = useRef()
66+
const keyRef = useRef(key)
4167
const methodRef = useRef(method)
4268
const onSuccessRef = useRef(onSuccess)
4369
const onErrorRef = useRef(onError)
@@ -46,26 +72,28 @@ const useDataLoader = (
4672
const isIdle = useMemo(() => status === StatusEnum.IDLE, [status])
4773
const isSuccess = useMemo(() => status === StatusEnum.SUCCESS, [status])
4874
const isError = useMemo(() => status === StatusEnum.ERROR, [status])
75+
4976
const isPolling = useMemo(
5077
() => enabled && pollingInterval && (isSuccess || isLoading),
5178
[isSuccess, isLoading, enabled, pollingInterval],
5279
)
5380

54-
const handleRequest = useRef(async (cacheKey, args) => {
55-
const cachedData = getCachedData(cacheKey)
56-
if (cacheKey && !data && cachedData) {
57-
dispatch(Actions.createOnUpdateData(cachedData))
58-
}
81+
const handleRequest = useRef(async cacheKey => {
5982
try {
6083
dispatch(Actions.createOnLoading())
61-
const result = await methodRef.current?.(args)
84+
const result = await methodRef.current?.()
6285

63-
if (result && cacheKey) addCachedData(cacheKey, result)
6486
if (keyRef.current && cacheKey && cacheKey !== keyRef.current) {
6587
return
6688
}
6789

68-
dispatch(Actions.createOnSuccess(result))
90+
if (keepPreviousData) {
91+
previousDataRef.current = getCachedData(cacheKey)
92+
}
93+
if (result !== undefined && result !== null && cacheKey)
94+
addCachedData(cacheKey, result)
95+
96+
dispatch(Actions.createOnSuccess())
6997

7098
await onSuccessRef.current?.(result)
7199
} catch (err) {
@@ -77,19 +105,15 @@ const useDataLoader = (
77105
useEffect(() => {
78106
let handler
79107
if (enabled) {
80-
if (
81-
reloadOnKeyChange &&
82-
key !== keyRef.current &&
83-
status !== StatusEnum.IDLE
84-
) {
108+
if (!isIdle && keyRef.current !== key) {
85109
keyRef.current = key
86-
dispatch(Actions.createReset({ data: initialData }))
110+
dispatch(Actions.createReset())
87111
} else {
88-
if (status === StatusEnum.IDLE) {
112+
if (isIdle) {
89113
keyRef.current = key
90114
handleRequest.current(key)
91115
}
92-
if (pollingInterval && status === StatusEnum.SUCCESS) {
116+
if (pollingInterval && isSuccess && !handler) {
93117
handler = setTimeout(
94118
() => handleRequest.current(key),
95119
pollingInterval,
@@ -107,38 +131,34 @@ const useDataLoader = (
107131
}
108132
if (handler) {
109133
clearTimeout(handler)
134+
handler = undefined
110135
}
111136
}
112137
// Why can't put empty array for componentDidMount, componentWillUnmount ??? No array act like componentDidMount and componentDidUpdate
113138
}, [
114139
enabled,
115-
pollingInterval,
116140
key,
117141
clearReload,
118142
addReload,
119-
status,
120-
reloadOnKeyChange,
121-
initialData,
143+
addCachedData,
144+
getCachedData,
145+
pollingInterval,
146+
isIdle,
147+
isSuccess,
122148
])
123149

124150
useLayoutEffect(() => {
125151
methodRef.current = method
126152
}, [method])
127153

128-
useLayoutEffect(() => {
129-
if (keepPreviousData && data && previousDataRef.current !== data) {
130-
previousDataRef.current = data
131-
}
132-
}, [keepPreviousData, data])
133-
134154
return {
135155
isLoading,
136156
isIdle,
137157
isSuccess,
138158
isError,
139159
isPolling,
140160
previousData: previousDataRef.current,
141-
data,
161+
data: getCachedData(key) || initialData,
142162
error,
143163
reload: args => handleRequest.current(key, args),
144164
}

0 commit comments

Comments
 (0)