|
| 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