Skip to content

Commit b86e7a2

Browse files
committed
initial commit
0 parents  commit b86e7a2

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+5873
-0
lines changed

.gitignore

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.js
7+
8+
# testing
9+
/coverage
10+
11+
# production
12+
/build
13+
14+
# misc
15+
.idea
16+
.vscode
17+
.DS_Store
18+
.env.local
19+
.env.development.local
20+
.env.test.local
21+
.env.production.local
22+
23+
npm-debug.log*
24+
yarn-debug.log*
25+
yarn-error.log*

LICENSE

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
ISC License
2+
3+
Copyright (c) 2020, Andrei Duca
4+
5+
Permission to use, copy, modify, and/or distribute this software for any
6+
purpose with or without fee is hereby granted, provided that the above
7+
copyright notice and this permission notice appear in all copies.
8+
9+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

README.md

Lines changed: 354 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,354 @@
1+
# useAsyncResource - data fetching hook for React Suspense
2+
3+
Convert any function that returns a Promise into a data reader function.
4+
The data reader can then be consumed by a "suspendable" React component.
5+
6+
The hook also returns an updater handler that triggers new api calls.
7+
The handler refreshes the data reader with each call.
8+
9+
10+
## ✨ Basic usage
11+
12+
```
13+
yarn add use-async-resource
14+
```
15+
16+
then:
17+
18+
```tsx
19+
import { useAsyncResource } from 'use-async-resource';
20+
21+
// a simple api function that fetches a user
22+
const fetchUser = (id: number) => fetch(`.../get/user/by/${id}`).then(res => res.json());
23+
24+
function App() {
25+
// 👉 initialize the data reader and start fetching the user immediately
26+
const [userReader, getNewUser] = useAsyncResource(fetchUser, 1);
27+
28+
return (
29+
<>
30+
<ErrorBoundary>
31+
<React.Suspense fallback="user is loading...">
32+
<User userReader={userReader} /* 👈 pass it to a suspendable child component */ />
33+
</React.Suspense>
34+
</ErrorBoundary>
35+
<button onClick={() => getNewUser(2)}>Get user with id 2</button>
36+
{/* clicking the button 👆 will start fetching a new user */}
37+
</>
38+
);
39+
}
40+
41+
function User({ userReader }) {
42+
const userData = userReader(); // 😎 just call the data reader function to get the user object
43+
44+
return <div>{userData.name}</div>;
45+
}
46+
```
47+
48+
49+
### Data Reader and Refresh handler
50+
51+
The `useAsyncResource` hook returns a pair:
52+
- the **data reader function**, which returns the expected result, or throws if the result is not yet available;
53+
- a **refresh handler to fetch new data** with new parameters.
54+
55+
The returned data reader `userReader` is a function that returns the user object if the api call completed successfully.
56+
57+
If the api call has not finished, the data reader function throws the promise, which is caught by the `React.Suspense` boundary.
58+
Suspense will retry to render the child component until it's successful, meaning the promised completed, the data is available, and the data reader doesn't throw anymore.
59+
60+
If the api call fails with an error, that error is thrown, and the `ErrorBoundary` component will catch it.
61+
62+
The refresh handler is identical with the original wrapped function, except it doesn't return anything - it only triggers new api calls.
63+
The data is retrievable with the data reader function.
64+
65+
Notice the returned items are a pair, so you can name them whatever you want, using the array destructuring:
66+
67+
```tsx
68+
const [userReader, getUser] = useAsyncResource(fetchUser, id);
69+
70+
const [postsReader, getPosts] = useAsyncResource(fetchPosts, category);
71+
72+
const [commentsReader, getComments] = useAsyncResource(fetchPostComments, postId, { orderBy: "date", order: "desc" });
73+
```
74+
75+
76+
### Api functions that don't accept parameters
77+
78+
If the api function doesn't accept any parameters, just pass an empty array as the second argument:
79+
80+
```tsx
81+
const fetchToggles = () => fetch('/path/to/global/toggles').then(res => res.json());
82+
83+
// in App.jsx
84+
const [toggles] = useAsyncResource(fetchToggles, []);
85+
```
86+
87+
Just like before, the api call is immediately invoked and the `toggles` data reader can be passed to a suspendable child component.
88+
89+
90+
## 🦥 Lazy initialization
91+
92+
All of the above examples are eagerly initialized, meaning the data starts fetching as soon as the `useAsyncResource` is called.
93+
But in some cases you would want to start fetching data only after a user interaction.
94+
95+
To lazily initialize the data reader, just pass the api function without any parameters:
96+
97+
```tsx
98+
const [userReader, getUserDetails] = useAsyncResource(fetchUserDetails);
99+
```
100+
101+
Then use the refresh handler to start fetching data when needed:
102+
103+
```tsx
104+
const [selectedUserId, setUserId] = React.useState();
105+
106+
const selectUserHandler = React.useCallback((userId) => {
107+
setUserId(userId);
108+
getUserDetails(userId); // 👈 call the refresh handler to trigger new api calls
109+
}, []);
110+
111+
return (
112+
<>
113+
<UsersList onUserItemClick={selectUserHandler} />
114+
{selectedUserId && (
115+
<React.Suspense>
116+
<UserDetails userReader={userReader} />
117+
</React.Suspense>
118+
)}
119+
</>
120+
);
121+
```
122+
123+
The only difference between a lazy data reader and an eagerly initialized one is that
124+
the lazy data reader can also return `undefined` if the data fetching hasn't stared yet.
125+
126+
Be aware of this difference when consuming the data in the child component:
127+
128+
```tsx
129+
function UserDetails({ userReader }) {
130+
const userData = userReader();
131+
// 👆 this may be `undefined` at first, so we need to check for it
132+
133+
if (userData === undefined) {
134+
return null;
135+
}
136+
137+
return <div>{userData.username} - {userData.email}</div>
138+
}
139+
```
140+
141+
142+
## 📦 Resource caching
143+
144+
All resources are cached, so subsequent calls with the same parameters for the same api function
145+
return the same resource, and don't trigger new, identical api calls.
146+
147+
This means you can write code like this, without having to think about deduplicating requests for the same user id:
148+
149+
```tsx
150+
function App() {
151+
// just like before, start fetching posts
152+
const [postsReader] = useAsyncResource(fetchPosts, []);
153+
154+
return (
155+
<React.Suspense fallback="loading posts">
156+
<Posts dataReader={postsReader} />
157+
</React.Suspense>
158+
);
159+
}
160+
161+
162+
function Posts(props) {
163+
// read the posts and render a list
164+
const postsList = props.dataReader();
165+
166+
return postsList.map(post => <Post post={post} />);
167+
}
168+
169+
170+
function Post(props) {
171+
// start fetching users for each individual post
172+
const [userReader] = useAsyncResource(fetchUser, props.post.authorId);
173+
// 👉 notice we don't need to deduplicate the user resource for potentially identical author ids
174+
175+
return (
176+
<article>
177+
<h1>{props.post.title}</h1>
178+
<React.Suspense fallback="loading author">
179+
<Author dataReader={userReader} />
180+
</React.Suspense>
181+
<p>{props.post.body}</p>
182+
</article>
183+
);
184+
}
185+
186+
187+
function Author(props) {
188+
// get the user object as usual
189+
const user = props.dataReader();
190+
191+
return <div>{user.displayName}</div>;
192+
}
193+
```
194+
195+
196+
### Clearing caches
197+
198+
In some instances however, you really need to re-fetch a resource (after updating a piece of data for example),
199+
so you'll need to clear the cached results. You can manually clear caches by using the `resourceCache` helper.
200+
201+
```tsx
202+
import { useAsyncResource, resourceCache } from 'use-async-resource';
203+
204+
// ...
205+
206+
const [latestPosts, getPosts] = useAsyncResource(fetchLatestPosts, []);
207+
208+
const refreshLatestPosts = React.useCallback(() => {
209+
// 🧹 clear the cache so we can make a new api call
210+
resourceCache(fetchLatestPosts).clear();
211+
// 🙌 refresh the data reader
212+
getPosts();
213+
}, []);
214+
```
215+
216+
In this case, we're clearing the entire cache for the `fetchLatestPosts` api function.
217+
But you can also pass parameters to the helper function, so you only delete the cache for those specific ones:
218+
219+
```tsx
220+
const [user, getUser] = useAsyncResource(fetchUser, id);
221+
222+
const refreshUserProfile = React.useCallback((userId) => {
223+
// only clear the cache for that id
224+
resourceCache(fetchUser, userId).delete();
225+
// get new user data
226+
getUser(userId);
227+
}, []);
228+
```
229+
230+
231+
## Data modifiers
232+
233+
When consumed, the data reader can take an optional argument: a function to modify the data.
234+
This function receives the original data as a parameter, and the transformation logic is up to you.
235+
236+
```tsx
237+
const userDisplayName = userDataReader(user => `${user.firstName} ${user.lastName}`);
238+
```
239+
240+
241+
## 📘 TypeScript support
242+
243+
The `useAsyncResource` hook infers all the types from the api function.
244+
The arguments it accepts after the api function are exactly the parameters of the original api function.
245+
246+
```tsx
247+
const fetchUser = (userId: number): Promise<UserType> => fetch('...');
248+
249+
const [wrongUserReader] = useAsyncResource(fetchUser, "some", "string", "params"); // 🚨 TS will complain about this
250+
const [correctUserReader] = useAsyncResource(fetchUser, 1); // 👌 just right
251+
const [lazyUserReader] = useAsyncResource(fetchUser); // 🦥 also ok, but lazily initialized
252+
```
253+
254+
The only exception is the api function without parameters:
255+
- the hook doesn't accept any other arguments than the api function, meaning it's lazily initialized;
256+
- or it accepts a single extra argument, an empty array, when we want the resource to start loading immediately.
257+
258+
```tsx
259+
const [lazyToggles] = useAsyncResource(fetchToggles); // 🦥 ok, but lazily initialized
260+
const [eagerToggles] = useAsyncResource(fetchToggles, []); // 🚀 ok, starts fetching immediately
261+
const [wrongToggles] = useAsyncResource(fetchToggles, "some", "params"); // 🚨 TS will complain about this
262+
```
263+
264+
265+
### Type inference for the data reader
266+
267+
The data reader will return exactly the type the original api function returns as a Promise.
268+
269+
```tsx
270+
const fetchUser = (userId: number): Promise<UserType> => fetch('...');
271+
272+
const [userReader] = useAsyncResource(fetchUser, 1);
273+
```
274+
275+
`userReader` is inferred as `() => UserType`, meaning a `function` that returns a `UserType` object.
276+
277+
If the resource is lazily initialized, the `userReader` can also return `undefined`:
278+
279+
```tsx
280+
const [userReader] = useAsyncResource(fetchUser);
281+
```
282+
283+
Here, `userReader` is inferred as `() => (UserType | undefined)`, meaning a `function` that returns either a `UserType` object, or `undefined`.
284+
285+
286+
### Type inference for the refresh handler
287+
288+
Not just the data reader types are inferred, but also the arguments of the refresh handler:
289+
290+
```tsx
291+
const fetchUser = (userId: number): Promise<UserType> => fetch('...');
292+
293+
const [userReader, getNewUser] = useAsyncResource(fetchUser, 1);
294+
```
295+
296+
The `getNewUser` handler is inferred as `(userId: number) => void`, meaning a `function` that takes a numeric argument `userId`, but doesn't return anything.
297+
298+
Remember: the return type of the handler is always `void`, because the handler only kicks off new data api calls.
299+
The data is still retrievable via the data reader function.
300+
301+
302+
## Default Suspense and ErrorBoundary wrappers
303+
304+
Again, a component consuming a data reader needs to be wrapped in both a `React.Suspense` boundary and a custom `ErrorBoundary`.
305+
306+
For convenience, you can use the bundled `AsyncResourceContent` that provides both:
307+
308+
```tsx
309+
import { useAsyncResource, AsyncResourceContent } from 'use-async-resource';
310+
311+
// ...
312+
313+
<AsyncResourceContent
314+
fallback="loading your data..."
315+
errorMessage="Some generic message when bad things happen"
316+
>
317+
<SomeComponent consuming={dataReader} />
318+
</AsyncResourceContent>
319+
```
320+
321+
The `fallback` can be a `string` or a React component.
322+
323+
The `errorMessage` can be either a `string`, a React component,
324+
or a function that takes the thrown error as an argument and returns a `string` or a React component.
325+
326+
```tsx
327+
<AsyncResourceContent
328+
fallback={<Spinner />}
329+
errorMessage={(e: CustomErrorType) => <span style={{ color: 'red' }}>{e.message}</span>}
330+
>
331+
<SomeComponent consuming={dataReader} />
332+
</AsyncResourceContent>
333+
```
334+
335+
336+
### Custom Error Boundary
337+
338+
Optionally, you can pass a custom error boundary component to be used instead of the default one:
339+
340+
```tsx
341+
class MyCustomErrorBoundary extends React.Component { ... }
342+
343+
// ...
344+
345+
<AsyncResourceContent
346+
// ...
347+
errorComponent={MyCustomErrorBoundary}
348+
errorMessage={/* optional error message */}
349+
>
350+
<SomeComponent consuming={dataReader} />
351+
</AsyncResourceContent>
352+
```
353+
354+
If you also pass the `errorMessage` prop, your custom error boundary will receive it.

0 commit comments

Comments
 (0)