UNPKG

5.8 kB Markdown View Raw
1---
2title: Client Data
3---
4
5# Client Data
6
7[MODES: framework]
8
9You can fetch and mutate data directly in the browser using `clientLoader` and `clientAction` functions.
10
11These functions are the primary mechanism for data handling when using [SPA mode][spa]. This guide demonstrates common use cases for leveraging client data in Server-Side Rendering (SSR).
12
13## Skip the Server Hop
14
15When using React Router with a Backend-For-Frontend (BFF) architecture, you might want to bypass the React Router server and communicate directly with your backend API. This approach requires proper authentication handling and assumes no CORS restrictions. Here's how to implement this:
16
171. Load the data from server `loader` on the document load
182. Load the data from the `clientLoader` on all subsequent loads
19
20In this scenario, React Router will _not_ call the `clientLoader` on hydration - and will only call it on subsequent navigations.
21
22```tsx lines=[4,11]
23export async function loader({
24 request,
25}: Route.LoaderArgs) {
26 const data = await fetchApiFromServer({ request }); // (1)
27 return data;
28}
29
30export async function clientLoader({
31 request,
32}: Route.ClientLoaderArgs) {
33 const data = await fetchApiFromClient({ request }); // (2)
34 return data;
35}
36```
37
38## Fullstack State
39
40Sometimes you need to combine data from both the server and browser (like IndexedDB or browser SDKs) before rendering a component. Here's how to implement this pattern:
41
421. Load the partial data from server `loader` on the document load
432. Export a [`HydrateFallback`][hydratefallback] component to render during SSR because we don't yet have a full set of data
443. Set `clientLoader.hydrate = true`, this instructs React Router to call the clientLoader as part of initial document hydration
454. Combine the server data with the client data in `clientLoader`
46
47```tsx lines=[4-6,19-20,23,26]
48export async function loader({
49 request,
50}: Route.LoaderArgs) {
51 const partialData = await getPartialDataFromDb({
52 request,
53 }); // (1)
54 return partialData;
55}
56
57export async function clientLoader({
58 request,
59 serverLoader,
60}: Route.ClientLoaderArgs) {
61 const [serverData, clientData] = await Promise.all([
62 serverLoader(),
63 getClientData(request),
64 ]);
65 return {
66 ...serverData, // (4)
67 ...clientData, // (4)
68 };
69}
70clientLoader.hydrate = true as const; // (3)
71
72export function HydrateFallback() {
73 return <p>Skeleton rendered during SSR</p>; // (2)
74}
75
76export default function Component({
77 // This will always be the combined set of server + client data
78 loaderData,
79}: Route.ComponentProps) {
80 return <>...</>;
81}
82```
83
84## Choosing Server or Client Data Loading
85
86You can mix data loading strategies across your application, choosing between server-only or client-only data loading for each route. Here's how to implement both approaches:
87
881. Export a `loader` when you want to use server data
892. Export `clientLoader` and a `HydrateFallback` when you want to use client data
90
91A route that only depends on a server loader looks like this:
92
93```tsx filename=app/routes/server-data-route.tsx
94export async function loader({
95 request,
96}: Route.LoaderArgs) {
97 const data = await getServerData(request);
98 return data;
99}
100
101export default function Component({
102 loaderData, // (1) - server data
103}: Route.ComponentProps) {
104 return <>...</>;
105}
106```
107
108A route that only depends on a client loader looks like this.
109
110```tsx filename=app/routes/client-data-route.tsx
111export async function clientLoader({
112 request,
113}: Route.ClientLoaderArgs) {
114 const clientData = await getClientData(request);
115 return clientData;
116}
117// Note: you do not have to set this explicitly - it is implied if there is no `loader`
118clientLoader.hydrate = true;
119
120// (2)
121export function HydrateFallback() {
122 return <p>Skeleton rendered during SSR</p>;
123}
124
125export default function Component({
126 loaderData, // (2) - client data
127}: Route.ComponentProps) {
128 return <>...</>;
129}
130```
131
132## Client-Side Caching
133
134You can implement client-side caching (using memory, localStorage, etc.) to optimize server requests. Here's a pattern that demonstrates cache management:
135
1361. Load the data from server `loader` on the document load
1372. Set `clientLoader.hydrate = true` to prime the cache
1383. Load subsequent navigations from the cache via `clientLoader`
1394. Invalidate the cache in your `clientAction`
140
141Note that since we are not exporting a `HydrateFallback` component, we will SSR the route component and then run the `clientLoader` on hydration, so it's important that your `loader` and `clientLoader` return the same data on initial load to avoid hydration errors.
142
143```tsx lines=[4,26,32,39,46]
144export async function loader({
145 request,
146}: Route.LoaderArgs) {
147 const data = await getDataFromDb({ request }); // (1)
148 return data;
149}
150
151export async function action({
152 request,
153}: Route.ActionArgs) {
154 await saveDataToDb({ request });
155 return { ok: true };
156}
157
158let isInitialRequest = true;
159
160export async function clientLoader({
161 request,
162 serverLoader,
163}: Route.ClientLoaderArgs) {
164 const cacheKey = generateKey(request);
165
166 if (isInitialRequest) {
167 isInitialRequest = false;
168 const serverData = await serverLoader();
169 cache.set(cacheKey, serverData); // (2)
170 return serverData;
171 }
172
173 const cachedData = await cache.get(cacheKey);
174 if (cachedData) {
175 return cachedData; // (3)
176 }
177
178 const serverData = await serverLoader();
179 cache.set(cacheKey, serverData);
180 return serverData;
181}
182clientLoader.hydrate = true; // (2)
183
184export async function clientAction({
185 request,
186 serverAction,
187}: Route.ClientActionArgs) {
188 const cacheKey = generateKey(request);
189 cache.delete(cacheKey); // (4)
190 const serverData = await serverAction();
191 return serverData;
192}
193```
194
195[spa]: ../how-to/spa
196[hydratefallback]: ../start/framework/route-module#hydratefallback
197
\No newline at end of file