Render as you Fetch
A core design feature of Reactive Data Client is decoupling actual data retrieval from data usage. This means hooks that want to ensure data availability like useFetch() or useSuspense() actually only dispatch the request to fetch. NetworkManager then uses its global awareness to determine whether to fetch. This means, for instance, that duplicate requests for data can be deduped into one fetch, with one promise to resolve.
Another interesting implication is that fetches started imperatively via Controller.fetch() won't result in redundant fetches. This is known as 'fetch as you render,' and often results in an improved user experience.
These are some scenarios where this pattern is especially useful:
- Server Side Rendering
- Loading data in parallel with code
- Concurrent Mode
Fetch-as-you-render can be adopted incrementally. Components using data can useSuspense() and be assured they will get their data when it's ready. And when render-as-you-fetch optimizations are added later - those components don't need to change. This makes data usage tightly coupled, and fetch optimization loosely coupled.
Routes that preload
In most cases the best time to pre-fetch data is at the routing layer. Doing this makes incorporating all of the above capabilities quite easy.
Use Controller.fetchIfStale in the route event handler (before startTransition)
import { Controller } from '@data-client/core';
import { lazy, Route } from '@anansi/router';
import { getImage } from '@data-client/img';
export const routes: Route<Controller>[] = [
{
name: 'UserDetail',
component: lazyPage('UserDetail'),
resolveData: async (controller: Controller, match: { id: string }) => {
if (match) {
const fakeUser = UserResource.fromJS({
id: Number.parseInt(match.id, 10),
});
// don't block on posts but start fetching
controller.fetchIfStale(PostResource.getList, { userId: match.id });
await Promise.all([
controller.fetchIfStale(UserResource.get, match),
controller.fetchIfStale(getImage, {
src: fakeUser.profileImage,
}),
controller.fetchIfStale(getImage, {
src: fakeUser.coverImage,
}),
controller.fetchIfStale(getImage, {
src: fakeUser.coverImageFallback,
}),
]);
}
},
},
];
Components using data
import { useSuspense } from '@data-client/react';
import { Img } from '@data-client/img';
import { Card, Avatar } from 'antd';
import { UserResource } from 'resources/Discuss';
import Boundary from 'Boundary';
import PostList from 'pages/Posts';
export type Props = { id: string };
const { Meta } = Card;
export default function UserDetail({ id }: Props) {
const user = useSuspense(UserResource.get, { id });
return (
<>
<Card cover={<Img src={user.coverImage} />}>
<Meta
avatar={<Img component={Avatar} src={user.profileImage} size={64} />}
title={user.name}
description={
<>
<div>{user.website}</div>
<div>{user.company.catchPhrase}</div>
</>
}
/>
</Card>
<Boundary fallback={<CardLoading />}>
<PostList userId={user.pk()} />
</Boundary>
</>
);
}
export function CardLoading() {
return <Card style={{ marginTop: 16 }} loading={true} />;
}