An open API service indexing awesome lists of open source software.

https://github.com/andreiduca/use-async-resource

A custom React hook for simple data fetching with React Suspense
https://github.com/andreiduca/use-async-resource

async cache custom-hook data data-fetching fetch hooks react react-cache react-hook react-hooks react-suspense reactjs suspense

Last synced: 5 months ago
JSON representation

A custom React hook for simple data fetching with React Suspense

Awesome Lists containing this project

README

        

# useAsyncResource - data fetching hook for React Suspense

Convert any function that returns a Promise into a data reader function.
The data reader can then be consumed by a "suspendable" React component.

The hook also returns an updater handler that triggers new api calls.
The handler refreshes the data reader with each call.

## ✨ Basic usage

```
yarn add use-async-resource
```

then:

```tsx
import { useAsyncResource } from 'use-async-resource';

// a simple api function that fetches a user
const fetchUser = (id: number) => fetch(`.../get/user/by/${id}`).then(res => res.json());

function App() {
// πŸ‘‰ initialize the data reader and start fetching the user immediately
const [userReader, getNewUser] = useAsyncResource(fetchUser, 1);

return (
<>





getNewUser(2)}>Get user with id 2
{/* clicking the button πŸ‘† will start fetching a new user */}
>
);
}

function User({ userReader }) {
const userData = userReader(); // 😎 just call the data reader function to get the user object

return

{userData.name}
;
}
```

### Data Reader and Refresh handler

The `useAsyncResource` hook returns a pair:
- the **data reader function**, which returns the expected result, or throws if the result is not yet available;
- a **refresh handler to fetch new data** with new parameters.

The returned data reader `userReader` is a function that returns the user object if the api call completed successfully.

If the api call has not finished, the data reader function throws the promise, which is caught by the `React.Suspense` boundary.
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.

If the api call fails with an error, that error is thrown, and the `ErrorBoundary` component will catch it.

The refresh handler is identical with the original wrapped function, except it doesn't return anything - it only triggers new api calls.
The data is retrievable with the data reader function.

Notice the returned items are a pair, so you can name them whatever you want, using the array destructuring:

```tsx
const [userReader, getUser] = useAsyncResource(fetchUser, id);

const [postsReader, getPosts] = useAsyncResource(fetchPosts, category);

const [commentsReader, getComments] = useAsyncResource(fetchPostComments, postId, { orderBy: "date", order: "desc" });
```

### Api functions that don't accept parameters

If the api function doesn't accept any parameters, just pass an empty array as the second argument:

```tsx
const fetchToggles = () => fetch('/path/to/global/toggles').then(res => res.json());

// in App.jsx
const [toggles] = useAsyncResource(fetchToggles, []);
```

Just like before, the api call is immediately invoked and the `toggles` data reader can be passed to a suspendable child component.

## πŸ¦₯ Lazy initialization

All of the above examples are eagerly initialized, meaning the data starts fetching as soon as the `useAsyncResource` is called.
But in some cases you would want to start fetching data only after a user interaction.

To lazily initialize the data reader, just pass the api function without any parameters:

```tsx
const [userReader, getUserDetails] = useAsyncResource(fetchUserDetails);
```

Then use the refresh handler to start fetching data when needed:

```tsx
const [selectedUserId, setUserId] = React.useState();

const selectUserHandler = React.useCallback((userId) => {
setUserId(userId);
getUserDetails(userId); // πŸ‘ˆ call the refresh handler to trigger new api calls
}, []);

return (
<>

{selectedUserId && (



)}
>
);
```

The only difference between a lazy data reader and an eagerly initialized one is that
the lazy data reader can also return `undefined` if the data fetching hasn't stared yet.

Be aware of this difference when consuming the data in the child component:

```tsx
function UserDetails({ userReader }) {
const userData = userReader();
// πŸ‘† this may be `undefined` at first, so we need to check for it

if (userData === undefined) {
return null;
}

return

{userData.username} - {userData.email}

}
```

## πŸ“¦ Resource caching

All resources are cached, so subsequent calls with the same parameters for the same api function
return the same resource, and don't trigger new, identical api calls.

This is useful for many reasons. First, it means you don't have to necessarily initialize the data reader
in a parent component. You only have to wrap the child component in a Suspense boundary:

```tsx
function App() {
return (



);
}

function Posts(props) {
// as usual, initialize the data reader and start fetching the posts
const [postsReader] = useAsyncResource(fetchPosts, []);

// now read the posts and render a list
const postsList = postsReader();

return postsList.map(post => );
}
```

This still works as you'd expect, even if the `App` component re-renders for any other reason,
before, during or even after the posts have loaded. Because the data reader gets cached, only the first initialization will trigger an api call.

This also means you can write code like this, without having to think about deduplicating requests for the same user id:

```tsx
function App() {
// just like before, start fetching posts
const [postsReader] = useAsyncResource(fetchPosts, []);

return (



);
}

function Posts(props) {
// read the posts and render a list
const postsList = props.dataReader();

return postsList.map(post => );
}

function Post(props) {
// start fetching users for each individual post
const [userReader] = useAsyncResource(fetchUser, props.post.authorId);
// πŸ‘‰ notice we don't need to deduplicate the user resource for potentially identical author ids

return (

{props.post.title}





{props.post.body}



);
}

function Author(props) {
// get the user object as usual
const user = props.dataReader();

return

{user.displayName}
;
}
```

### 🚚 Preloading resources

When you know a resource will be consumed by a child component, you can preload it ahead of time.
This is useful in cases such as lazy loaded components, or when trying to predict a user's intent.

```tsx
// πŸ‘‰ import the `preloadResource` helper
import { useAsyncResource, preloadResource } from 'use-async-resource';

// a lazy-loaded React component
const PostsList = React.lazy(() => import('./PostsListComponent'));

// some api function
const fetchUserPosts = (userId) => fetch(`/path/to/get/user/${userId}/posts`).then(res => res.json())

function UserProfile(props) {
const [showPostsList, toggleList] = React.useState(false);

return (
<>

{props.user.name}


toggleList(true)}
// πŸ‘‰ we can preload the resource as soon as the user
// shows any intent of interacting with the button
onMouseOver={() => preloadResource(fetchUserPosts, props.user.id)}
>
show user posts

{showPostsList && (
// this child will suspend if either:
// - the `PostList` component code is not yet loaded
// - or the data reader inside it is not yet ready
// πŸ‘‰ notice we're not initializing any resource to pass it to the child component



)}
>
);
}

// in PostsListComponent.tsx
function PostsList(props) {
// πŸ‘‰ instead, we initialize the data reader inside the child component directly
const [posts] = useAsyncResource(fetchUserPosts, props.userId);

// ✨ because we preloaded it in the parent with the same `userId` parameter,
// it will get initialized with that cached version

// also, the outer React.Suspense boundary in the parent will take care of rendering the fallback
return (


    {posts().map(post =>
  • )}

);
}
```

In the above example, even if the child component loads faster than the data,
re-rendering it multiple times until the data is ready is ok, because every time
the data reader will be initialized from the same cached version.
No api call will ever be triggered from the child component,
because that happened in the parent when the user hovered the button.

At the same time, if the data is ready before the code loads, it will be available immediately
when the child component will render for the first time.

### Clearing caches

Finally, you can manually clear caches by using the `resourceCache` helper.

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

// ...

const [latestPosts, getPosts] = useAsyncResource(fetchLatestPosts, []);

const refreshLatestPosts = React.useCallback(() => {
// 🧹 clear the cache so we can make a new api call
resourceCache(fetchLatestPosts).clear();
// πŸ™Œ refresh the data reader
getPosts();
}, []);
```

In this case, we're clearing the entire cache for the `fetchLatestPosts` api function.
But you can also use the `delete()` method with parameters, so you only delete the cache for those specific ones:

```tsx
const [user, getUser] = useAsyncResource(fetchUser, id);

const refreshUserProfile = React.useCallback((userId) => {
// only clear the cache for that id
resourceCache(fetchUser).delete(userId);
// get new user data
getUser(userId);
}, []);
```

## Data modifiers

When consumed, the data reader can take an optional argument: a function to modify the data.
This function receives the original data as a parameter, and the transformation logic is up to you.

```tsx
const userDisplayName = userDataReader(user => `${user.firstName} ${user.lastName}`);
```

## File resource helpers

Suspense is not just about fetching data in a declarative way, but about fetching resources in general, including images and scripts.

The included `fileResource` helper will turn a URL string into a resource "data reader" function, but it will load a resource instead of data.
When the resource finishes loading, the "data reader" function will return the URL you passed in. Until then, it will throw a Promise, so Suspense can render a fallback.

Here's an example for an image resource:

```tsx
import { useAsyncResource, fileResourceΒ } from 'use-async-resource';

function Author({ user }) {
// initialize the image "data reader"
const [userImageReader] = useAsyncResource(fileResource.image, user.profilePicUrl);

return (

{/* render a fallback until the image is downloaded */}
}>
{/* pass the resource "data reader" to a suspendable component */}


{user.name}


{user.bio}



);
}

function ProfilePhoto(props) {
// just read back the URL and use it in an `img` tag when the image is ready
const imageSrc = props.resource();

return ;
}
```

Using the `fileResource` to load external scripts is just as easy:

```tsx
function App() {
const [jq] = useAsyncResource(fileResource.script, 'https://code.jquery.com/jquery-3.4.1.slim.min.js');

return (



);
}

function JQComponent(props) {
const jQ = props.jQueryResource();

// jQuery should be available and you can do something with it
return

jQuery version: {window.jQuery.fn.jquery}

}
```

Notice we don’t do anything with the `const jQ`, but we still need to call `props.jQueryResource()` so it can throw,
rendering the fallback until the library is fully loaded on the page.

## πŸ“˜ TypeScript support

The `useAsyncResource` hook infers all types from the api function passed in.
The arguments it accepts after the api function are exactly the parameters of the original api function.

```tsx
const fetchUser = (userId: number): Promise => fetch('...');

const [wrongUserReader] = useAsyncResource(fetchUser, "some", "string", "params"); // 🚨 TS will complain about this
const [correctUserReader] = useAsyncResource(fetchUser, 1); // πŸ‘Œ just right
const [lazyUserReader] = useAsyncResource(fetchUser); // πŸ¦₯ also ok, but lazily initialized
```

The only exception is the api function without parameters:
- the hook doesn't accept any other arguments than the api function, meaning it's lazily initialized;
- or it accepts a single extra argument, an empty array, when we want the resource to start loading immediately.

```tsx
const [lazyToggles] = useAsyncResource(fetchToggles); // πŸ¦₯ ok, but lazily initialized
const [eagerToggles] = useAsyncResource(fetchToggles, []); // πŸš€ ok, starts fetching immediately
const [wrongToggles] = useAsyncResource(fetchToggles, "some", "params"); // 🚨 TS will complain about this
```

### Type inference for the data reader

The data reader will return exactly the type the original api function returns as a Promise.

```tsx
const fetchUser = (userId: number): Promise => fetch('...');

const [userReader] = useAsyncResource(fetchUser, 1);
```

`userReader` is inferred as `() => UserType`, meaning a `function` that returns a `UserType` object.

If the resource is lazily initialized, the `userReader` can also return `undefined`:

```tsx
const [userReader] = useAsyncResource(fetchUser);
```

Here, `userReader` is inferred as `() => (UserType | undefined)`, meaning a `function` that returns either a `UserType` object, or `undefined`.

### Type inference for the refresh handler

Not just the data reader types are inferred, but also the arguments of the refresh handler:

```tsx
const fetchUser = (userId: number): Promise => fetch('...');

const [userReader, getNewUser] = useAsyncResource(fetchUser, 1);
```

The `getNewUser` handler is inferred as `(userId: number) => void`, meaning a `function` that takes a numeric argument `userId`, but doesn't return anything.

Remember: the return type of the handler is always `void`, because the handler only kicks off new data api calls.
The data is still retrievable via the data reader function.

## Default Suspense and ErrorBoundary wrappers

Again, a component consuming a data reader needs to be wrapped in both a `React.Suspense` boundary and a custom `ErrorBoundary`.

For convenience, you can use the bundled `AsyncResourceContent` that provides both:

```tsx
import { useAsyncResource, AsyncResourceContent } from 'use-async-resource';

// ...

```

The `fallback` can be a `string` or a React component.

The `errorMessage` can be either a `string`, a React component,
or a function that takes the thrown error as an argument and returns a `string` or a React component.

```tsx
}
errorMessage={(e: CustomErrorType) => {e.message}}
>

```

### Custom Error Boundary

Optionally, you can pass a custom error boundary component to be used instead of the default one:

```tsx
class MyCustomErrorBoundary extends React.Component { ... }

// ...

```

If you also pass the `errorMessage` prop, your custom error boundary will receive it as a prop.