Skip to content

Commit ef8beb4

Browse files
authored
update the data reader in a useMemo instead of a useEffect (#8)
1 parent 20f4206 commit ef8beb4

File tree

6 files changed

+69
-76
lines changed

6 files changed

+69
-76
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ function Author(props) {
220220
```
221221

222222

223-
### Preloading resources
223+
### 🚚 Preloading resources
224224

225225
When you know a resource will be consumed by a child component, you can preload it ahead of time.
226226
This is useful in cases such as lazy loaded components, or when trying to predict a user's intent.
@@ -291,10 +291,10 @@ because that happened in the parent when the user hovered the button.
291291
At the same time, if the data is ready before the code loads, it will be available immediately
292292
when the child component will render for the first time.
293293

294+
294295
### Clearing caches
295296

296-
In some instances however, you really need to re-fetch a resource (after updating a piece of data for example),
297-
so you'll need to clear the cached results. You can manually clear caches by using the `resourceCache` helper.
297+
Finally, you can manually clear caches by using the `resourceCache` helper.
298298

299299
```tsx
300300
import { useAsyncResource, resourceCache } from 'use-async-resource';

lib/useAsyncResource.js

Lines changed: 18 additions & 23 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/useAsyncResource.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "use-async-resource",
3-
"version": "2.1.0",
3+
"version": "2.2.0",
44
"description": "A custom React hook for simple data fetching with React Suspense",
55
"keywords": [
66
"react",

src/useAsyncResource.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@ import { suspendFor } from './test.helpers';
99
describe('useAsyncResource', () => {
1010
const apiFn = (id: number) => Promise.resolve({ id, name: 'test name' });
1111
const apiSimpleFn = () => Promise.resolve({ message: 'success' });
12+
const apiFailingFn = () => Promise.reject({ message: 'error' });
1213

1314
afterEach(() => {
1415
resourceCache(apiFn).clear();
1516
resourceCache(apiSimpleFn).clear();
17+
resourceCache(apiFailingFn).clear();
1618
});
1719

1820
it('should create a new data reader', async () => {
@@ -33,6 +35,16 @@ describe('useAsyncResource', () => {
3335
expect(simpleData()).toStrictEqual({ message: 'success' });
3436
});
3537

38+
it('should throw an error', async () => {
39+
const { result } = renderHook(() => useAsyncResource(apiFailingFn, []));
40+
const [dataReader] = result.current;
41+
42+
// wait for it to fulfill
43+
await suspendFor(dataReader);
44+
45+
expect(dataReader).toThrowError(Error('error'));
46+
});
47+
3648
it('should trigger an update for the data reader', async () => {
3749
// get the data reader and the updater function from the custom hook
3850
const { result } = renderHook(() => useAsyncResource(apiFn, 1));

src/useAsyncResource.ts

Lines changed: 34 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useCallback, useEffect, useRef, useState } from 'react';
1+
import { useCallback, useMemo, useRef, useState } from 'react';
22

33
import {
44
ApiFn,
@@ -59,56 +59,42 @@ export function useAsyncResource<ResponseType, ArgTypes extends any[]>(
5959
apiFunction: ApiFn<ResponseType> | ApiFn<ResponseType, ArgTypes>,
6060
...parameters: ArgTypes
6161
) {
62-
const firstRender = useRef(true);
62+
// keep the data reader inside a mutable object ref
63+
// always initialize with a lazy data reader, as it can be overwritten by the useMemo immediately
64+
const dataReaderObj = useRef<DataOrModifiedFn<ResponseType> | LazyDataOrModifiedFn<ResponseType>>(() => undefined);
6365

64-
// initialize the data reader
65-
const [dataReader, updateDataReader] = useState(() => {
66-
// lazy initialization, when no parameters are passed
67-
if (!parameters.length) {
68-
// we return an empty data reader function
69-
return (() => undefined) as LazyDataOrModifiedFn<ResponseType>;
70-
}
71-
72-
// eager initialization for api functions that don't accept arguments
73-
if (
74-
// check that the api function doesn't take any arguments
75-
!apiFunction.length &&
76-
// but the user passed an empty array as the only parameter
77-
parameters.length === 1 &&
78-
Array.isArray(parameters[0]) &&
79-
parameters[0].length === 0
80-
) {
81-
return initializeDataReader(apiFunction as ApiFn<ResponseType>);
66+
// like useEffect, but runs immediately
67+
useMemo(() => {
68+
if (parameters.length) {
69+
// eager initialization for api functions that don't accept arguments
70+
if (
71+
// check that the api function doesn't take any arguments
72+
!apiFunction.length &&
73+
// but the user passed an empty array as the only parameter
74+
parameters.length === 1 &&
75+
Array.isArray(parameters[0]) &&
76+
parameters[0].length === 0
77+
) {
78+
dataReaderObj.current = initializeDataReader(apiFunction as ApiFn<ResponseType>);
79+
} else {
80+
// eager initialization for all other cases
81+
dataReaderObj.current = initializeDataReader(
82+
apiFunction as ApiFn<ResponseType, ArgTypes>,
83+
...parameters,
84+
);
85+
}
8286
}
87+
}, [apiFunction, ...parameters]);
8388

84-
// eager initialization for all other cases
85-
return initializeDataReader(
86-
apiFunction as ApiFn<ResponseType, ArgTypes>,
87-
...parameters,
88-
);
89-
});
89+
// state to force re-render
90+
const [, forceRender] = useState(0);
9091

91-
// the updater function
92-
const updater = useCallback(
93-
(...newParameters: ArgTypes) => {
94-
updateDataReader(() =>
95-
initializeDataReader(
96-
apiFunction as ApiFn<ResponseType, ArgTypes>,
97-
...newParameters,
98-
),
99-
);
100-
},
101-
[apiFunction],
102-
);
103-
104-
// automatically call the updater function every time the params of the hook change
105-
useEffect(() => {
106-
if (firstRender.current) {
107-
firstRender.current = false;
108-
} else if (apiFunction.length > 0) {
109-
updater(...parameters);
110-
}
111-
}, [...parameters]);
92+
const updaterFn = useCallback((...newParameters: ArgTypes) => {
93+
// update the object ref
94+
dataReaderObj.current = initializeDataReader(apiFunction as ApiFn<ResponseType, ArgTypes>, ...newParameters);
95+
// update state to force a re-render
96+
forceRender(ct => 1 - ct);
97+
}, [apiFunction]);
11298

113-
return [dataReader, updater];
99+
return [dataReaderObj.current, updaterFn];
114100
}

0 commit comments

Comments
 (0)