UNPKG

5.81 kB Markdown View Raw
1---
2title: Client Data
3---
4
5# Client Data
6
7[MODES: framework]
8
9<br/>
10<br/>
11
12You can fetch and mutate data directly in the browser using `clientLoader` and `clientAction` functions.
13
14These 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).
15
16## Skip the Server Hop
17
18When 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:
19
201. Load the data from server `loader` on the document load
212. Load the data from the `clientLoader` on all subsequent loads
22
23In this scenario, React Router will _not_ call the `clientLoader` on hydration - and will only call it on subsequent navigations.
24
25```tsx lines=[4,11]
26export async function loader({
27 request,
28}: Route.LoaderArgs) {
29 const data = await fetchApiFromServer({ request }); // (1)
30 return data;
31}
32
33export async function clientLoader({
34 request,
35}: Route.ClientLoaderArgs) {
36 const data = await fetchApiFromClient({ request }); // (2)
37 return data;
38}
39```
40
41## Fullstack State
42
43Sometimes 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:
44
451. Load the partial data from server `loader` on the document load
462. Export a [`HydrateFallback`][hydratefallback] component to render during SSR because we don't yet have a full set of data
473. Set `clientLoader.hydrate = true`, this instructs React Router to call the clientLoader as part of initial document hydration
484. Combine the server data with the client data in `clientLoader`
49
50```tsx lines=[4-6,19-20,23,26]
51export async function loader({
52 request,
53}: Route.LoaderArgs) {
54 const partialData = await getPartialDataFromDb({
55 request,
56 }); // (1)
57 return partialData;
58}
59
60export async function clientLoader({
61 request,
62 serverLoader,
63}: Route.ClientLoaderArgs) {
64 const [serverData, clientData] = await Promise.all([
65 serverLoader(),
66 getClientData(request),
67 ]);
68 return {
69 ...serverData, // (4)
70 ...clientData, // (4)
71 };
72}
73clientLoader.hydrate = true as const; // (3)
74
75export function HydrateFallback() {
76 return <p>Skeleton rendered during SSR</p>; // (2)
77}
78
79export default function Component({
80 // This will always be the combined set of server + client data
81 loaderData,
82}: Route.ComponentProps) {
83 return <>...</>;
84}
85```
86
87## Choosing Server or Client Data Loading
88
89You 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:
90
911. Export a `loader` when you want to use server data
922. Export `clientLoader` and a `HydrateFallback` when you want to use client data
93
94A route that only depends on a server loader looks like this:
95
96```tsx filename=app/routes/server-data-route.tsx
97export async function loader({
98 request,
99}: Route.LoaderArgs) {
100 const data = await getServerData(request);
101 return data;
102}
103
104export default function Component({
105 loaderData, // (1) - server data
106}: Route.ComponentProps) {
107 return <>...</>;
108}
109```
110
111A route that only depends on a client loader looks like this.
112
113```tsx filename=app/routes/client-data-route.tsx
114export async function clientLoader({
115 request,
116}: Route.ClientLoaderArgs) {
117 const clientData = await getClientData(request);
118 return clientData;
119}
120// Note: you do not have to set this explicitly - it is implied if there is no `loader`
121clientLoader.hydrate = true;
122
123// (2)
124export function HydrateFallback() {
125 return <p>Skeleton rendered during SSR</p>;
126}
127
128export default function Component({
129 loaderData, // (2) - client data
130}: Route.ComponentProps) {
131 return <>...</>;
132}
133```
134
135## Client-Side Caching
136
137You can implement client-side caching (using memory, localStorage, etc.) to optimize server requests. Here's a pattern that demonstrates cache management:
138
1391. Load the data from server `loader` on the document load
1402. Set `clientLoader.hydrate = true` to prime the cache
1413. Load subsequent navigations from the cache via `clientLoader`
1424. Invalidate the cache in your `clientAction`
143
144Note 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.
145
146```tsx lines=[4,26,32,39,46]
147export async function loader({
148 request,
149}: Route.LoaderArgs) {
150 const data = await getDataFromDb({ request }); // (1)
151 return data;
152}
153
154export async function action({
155 request,
156}: Route.ActionArgs) {
157 await saveDataToDb({ request });
158 return { ok: true };
159}
160
161let isInitialRequest = true;
162
163export async function clientLoader({
164 request,
165 serverLoader,
166}: Route.ClientLoaderArgs) {
167 const cacheKey = generateKey(request);
168
169 if (isInitialRequest) {
170 isInitialRequest = false;
171 const serverData = await serverLoader();
172 cache.set(cacheKey, serverData); // (2)
173 return serverData;
174 }
175
176 const cachedData = await cache.get(cacheKey);
177 if (cachedData) {
178 return cachedData; // (3)
179 }
180
181 const serverData = await serverLoader();
182 cache.set(cacheKey, serverData);
183 return serverData;
184}
185clientLoader.hydrate = true; // (2)
186
187export async function clientAction({
188 request,
189 serverAction,
190}: Route.ClientActionArgs) {
191 const cacheKey = generateKey(request);
192 cache.delete(cacheKey); // (4)
193 const serverData = await serverAction();
194 return serverData;
195}
196```
197
198[spa]: ../how-to/spa
199[hydratefallback]: ../start/framework/route-module#hydratefallback
200
\No newline at end of file