useSuspense()
High performance async data rendering without overfetching.
useSuspense()
is like await for React components. This means the remainder of the component only runs after the data has loaded, avoiding the complexity of handling loading and error conditions. Instead, fallback handling is
centralized with a singular AsyncBoundary.
useSuspense()
is reactive to data mutations; rerendering only when necessary.
Usage
- Rest
- Promise
import { useSuspense } from '@data-client/react'; import { ProfileResource } from './ProfileResource'; function ProfileDetail(): JSX.Element { const profile = useSuspense(ProfileResource.get, { id: 1 }); return ( <div className="listItem"> <Avatar src={profile.avatar} /> <div> <h4>{profile.fullName}</h4> <p>{profile.bio}</p> </div> </div> ); } render(<ProfileDetail />);
import { useSuspense } from '@data-client/react'; import { getProfile } from './Profile'; function ProfileDetail(): JSX.Element { const profile = useSuspense(getProfile, 1); return ( <div className="listItem"> <Avatar src={profile.avatar} /> <div> <h4>{profile.fullName}</h4> <p>{profile.bio}</p> </div> </div> ); } render(<ProfileDetail />);
Behavior
Cache policy is Stale-While-Revalidate by default but also configurable.
Expiry Status | Fetch | Suspend | Error | Conditions |
---|---|---|---|---|
Invalid | yes1 | yes | no | not in store, deletion, invalidation, invalidIfStale |
Stale | yes1 | no | no | (first-render, arg change) & expiry < now |
Valid | no | no | maybe2 | fetch completion |
no | no | no | null used as second argument |
- Identical fetches are automatically deduplicated
- Hard errors to be caught by Error Boundaries
When using React Navigation, useSuspense() will trigger fetches on focus if the data is considered stale.
Use null
as the second argument to any Data Client hook means "do nothing."
// todo could be undefined if id is undefined
const todo = useSuspense(TodoResource.get, id ? { id } : null);
Types
- Type
- With Generics
function useSuspense(
endpoint: ReadEndpoint,
...args: Parameters<typeof endpoint> | [null]
): Denormalize<typeof endpoint.schema>;
function useSuspense<
E extends EndpointInterface<
FetchFunction,
Schema | undefined,
undefined
>,
Args extends readonly [...Parameters<E>] | readonly [null],
>(
endpoint: E,
...args: Args
): E['schema'] extends Exclude<Schema, null>
? Denormalize<E['schema']>
: ReturnType<E>;
Examples
List
import { useSuspense } from '@data-client/react'; import { ProfileResource } from './ProfileResource'; function ProfileList(): JSX.Element { const profiles = useSuspense(ProfileResource.getList); return ( <div> {profiles.map(profile => ( <div className="listItem" key={profile.pk()}> <Avatar src={profile.avatar} /> <div> <h4>{profile.fullName}</h4> <p>{profile.bio}</p> </div> </div> ))} </div> ); } render(<ProfileList />);
Pagination
Reactive pagination is achieved with mutable schemas
import { useSuspense } from '@data-client/react'; import PostItem from './PostItem'; import LoadMore from './LoadMore'; import { PostResource } from './Post'; export default function PostList() { const { posts, cursor } = useSuspense(PostResource.getList); return ( <div> {posts.map(post => ( <PostItem key={post.pk()} post={post} /> ))} {cursor ? <LoadMore cursor={cursor} /> : null} </div> ); } render(<PostList />);
Sequential
When fetch parameters depend on data from another resource.
function PostWithAuthor() {
const post = useSuspense(PostResource.get, { id });
const author = useSuspense(UserResource.get, {
id: post.userId,
});
}
Conditional
null
will avoid binding and fetching data
import { PostResource, UserResource } from './Resources'; export default function PostWithAuthor({ id }: { id: string }) { const post = useSuspense(PostResource.get, { id }); const author = useSuspense( UserResource.get, post.userId ? { id: post.userId, } : null, ); // author as User | undefined if (!author) return; }
Embedded data
When entities are stored in nested structures, that structure will remain.
export class PaginatedPost extends Entity { id = ''; title = ''; content = ''; static key = 'PaginatedPost'; } export const getPosts = new RestEndpoint({ path: '/post', searchParams: { page: '' }, schema: { posts: new schema.Collection([PaginatedPost]), nextPage: '', lastPage: '', }, });
import { getPosts } from './api/Post'; export default function ArticleList({ page }: { page: string }) { const { posts, nextPage, lastPage, } = useSuspense(getPosts, { page }); return ( <div> {posts.map(post => ( <div key={post.pk()}>{post.title}</div> ))} </div> ); }
Server Side Rendering
Server Side Rendering to incrementally stream HTML, greatly reducing TTFB. Reactive Data Client SSR's automatic store hydration means immediate user interactivity with zero client-side fetches on first load.
Usage in components is identical, which means you can easily share components between SSR and non-SSR applications, as well as migrate to SSR without needing data-client code changes.
Concurrent Mode
In React 18 navigating with startTransition
allows AsyncBoundaries to
continue showing the previous screen while the new data loads. Combined with
streaming server side rendering, this eliminates the need to flash annoying
loading indicators - improving the user experience.
Click one of the names to navigate to their todos. Here long loading states are indicated by the less intrusive loading bar, like YouTube and Robinhood use.
If you need help adding this to your own custom router, check out the official React guide