Skip to content

Commit 637b023

Browse files
authored
Client Data Documentation Updates (#8183)
1 parent a2b26d3 commit 637b023

6 files changed

+238
-3
lines changed

.changeset/client-data.md

+18-1
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,21 @@
55
"@remix-run/testing": minor
66
---
77

8-
Add support for `clientLoader`/`clientAction` route exports ([RFC](https://github.com/remix-run/remix/discussions/7634))
8+
Add support for `clientLoader`/`clientAction`/`HydrateFallback` route exports ([RFC](https://github.com/remix-run/remix/discussions/7634)).
9+
10+
Remix now supports loaders/actions that run on the client (in addition to, or instead of the loader/action that runs on the server). While we still recommend server loaders/actions for the majority of your data needs in a Remix app - these provide some levers you can pull for more advanced use-cases such as:
11+
12+
- Leveraging a data source local to the browser (i.e., `localStorage`)
13+
- Managing a client-side cache of server data (like `IndexedDB`)
14+
- Bypassing the Remix server in a BFF setup nd hitting your API directly from the browser
15+
- Migrating a React Router SPA to a Remix application
16+
17+
By default, `clientLoader` will not run on hydration, and will only run on subsequent client side navigations.
18+
19+
If you wish to run your client loader on hydration, you can set `clientLoader.hydrate=true` to force Remix to execute it on initial page load. Keep in mind that Remix will still SSR your route component so you should ensure that there is no new _required_ data being added by your `clientLoader`.
20+
21+
If your `clientLoader` neds to run on hydration and adds data you require to render the route component, you can export a `HydrateFallback` component that will render during SSR, and then your route component will not render until the `clientLoader` has executed on hydration.
22+
23+
`clientAction1` is simpler than `clientLoader` because it has no hydration use-cases. `clientAction` will only run on client-side navigations.
24+
25+
For more information, please refer to the [`clientLoader`](https://remix.run/route/client-loader) and [`clientAction`](https://remix.run/route/client-action) documentation.

.changeset/data-function-args.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,4 @@
55
"@remix-run/server-deno": minor
66
---
77

8-
Deprecate `DataFunctionArgs` in favor of `LoaderFunctionArgs`/`ActionFunctionArgs`. This is aimed at keeping the types aligned across server/client loaders/actions now that we have `serverLoader`/`serverAction` parameters which differentiate `ClientLoaderFunctionArgs`/`ClientActionFunctionArgs`.
8+
Deprecate `DataFunctionArgs` in favor of `LoaderFunctionArgs`/`ActionFunctionArgs`. This is aimed at keeping the types aligned across server/client loaders/actions now that `clientLoader`/`clientActon` functions have `serverLoader`/`serverAction` parameters which differentiate `ClientLoaderFunctionArgs`/`ClientActionFunctionArgs`.

docs/route/client-action.md

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
---
2+
title: clientAction
3+
---
4+
5+
# `clientAction`
6+
7+
In addition to (or in place of) your [`action`][action], you may define a `clientAction` function that will execute on the client.
8+
9+
Each route can define a `clientAction` function that handles mutations:
10+
11+
```tsx
12+
export const clientAction = async ({
13+
request,
14+
params,
15+
serverAction,
16+
}: ClientActionFunctionArgs) => {
17+
invalidateClientSideCache();
18+
const data = await serverAction();
19+
return data;
20+
};
21+
```
22+
23+
This function is only ever run on the client, and can used in a few ways:
24+
25+
- Instead of a server action for full-client routes
26+
- To use alongside a `clientLoader` cache by invalidating the cache on mutations
27+
- To facilitate a migration from React Router
28+
29+
## Arguments
30+
31+
### `params`
32+
33+
This function receives the same [`params`][action-params] argument as an [`action`][action].
34+
35+
### `request`
36+
37+
This function receives the same [`request`][action-request] argument as an [`action`][action].
38+
39+
### `serverAction`
40+
41+
`serverAction` is an asynchronous function that makes the [fetch][fetch] call to the server `action` for this route.
42+
43+
[action]: ./action
44+
[action-params]: ./loader#params
45+
[action-request]: ./loader#request
46+
[fetch]: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API

docs/route/client-loader.md

+121
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
---
2+
title: clientLoader
3+
---
4+
5+
# `clientLoader`
6+
7+
In addition to (or in place of) your [`loader`][loader], you may define a `clientLoader` function that will execute on the client.
8+
9+
Each route can define a `clientLoader` function that provides data to the route when rendering:
10+
11+
```tsx
12+
export const clientLoader = async ({
13+
request,
14+
params,
15+
serverLoader,
16+
}: ClientLoaderFunctionArgs) => {
17+
// call the server loader
18+
const serverData = await serverLoader();
19+
// And/or fetch data on the client
20+
const data = getDataFromClient();
21+
// Return the data to expose through useLoaderData()
22+
return data;
23+
};
24+
```
25+
26+
This function is only ever run on the client, and can used in a few ways:
27+
28+
- Instead of a server action for full-client routes
29+
- To use alongside a `clientLoader` cache by invalidating the cache on mutations
30+
- Maintaining a client-side cache to skip calls to the server
31+
- Bypassing the Remix [BFF][bff] hop and hitting your API directly from the client
32+
- To further augment data loaded from the server
33+
- I.e., loading user-specific preferences from `localStorage`
34+
- To facilitate a migration from React Router
35+
36+
## Hydration Behavior
37+
38+
By default, `clientLoader` **will not** execute for the route during initial hydration. This is for the primary (and simpler) use-case where the `clientLoader` does not change the shape of the server `loader` data and is just an optimization on subsequent client side navigations (to read from a cache or hit an API directly).
39+
40+
```tsx
41+
export async function loader() {
42+
// During SSR, we talk to the DB directly
43+
const data = getServerDataFromDb();
44+
return json(data);
45+
}
46+
47+
export async function clientLoader() {
48+
// During client-side navigations, we hit our exposed API endpoints directly
49+
const data = await fetchDataFromApi();
50+
return data;
51+
}
52+
53+
export default function Component() {
54+
const data = useLoaderData<typeof loader>();
55+
return <>...</>;
56+
}
57+
```
58+
59+
### `clientLoader.hydrate`
60+
61+
If you need to run your `clientLoader` on hydration, you can opt-into that by setting `clientLoader.hydrate=true`. This will tell Remix that it needs to run the `clientLoader` on hydration. A common use-case for this is to prime a client-side cache with the data loaded on the server:
62+
63+
```tsx
64+
export async function loader() {
65+
const data = await getDataFromDB();
66+
return json(data);
67+
}
68+
69+
let isInitialHydration = true;
70+
export async function clientLoader({ serverLoader }) {
71+
if (isInitialHydration) {
72+
isInitialHydration = false;
73+
// This will resolve with the hydrated server data, it won't fetch()
74+
const serverData = await serverLoader();
75+
cache.set(cacheKey, serverData);
76+
return serverData;
77+
}
78+
79+
const cachedData = await cache.get(cacheKey);
80+
if (cachedData) {
81+
return cachedData;
82+
}
83+
84+
const data = await serverLoader();
85+
cache.set(cacheKey, data);
86+
return data;
87+
}
88+
clientLoader.hydrate = true;
89+
90+
export default function Component() {
91+
const data = useLoaderData<typeof loader>();
92+
return <>...</>;
93+
}
94+
```
95+
96+
<docs-info>If a route exports a `clientLoader` and does not export a server `loader`, then `clientLoader.hydrate` is automatically treated as `true` since there is no server data to SSR with. Therefore, we always need to run the `clientLoader` on hydration before rendering the route component.</docs-info>
97+
98+
### HydrateFallback
99+
100+
If you need to avoid rendering your default route component during SSR because you have data that must come from a `clientLoader`, you can export a [`HydrateFallback`][hydratefallback] component from your route that will be rendered during SSR, and only once the clientLoader runs on hydration will your router component be rendered.
101+
102+
## Arguments
103+
104+
### `params`
105+
106+
This function receives the same [`params`][loader-params] argument as a [`loader`][loader].
107+
108+
### `request`
109+
110+
This function receives the same [`request`][loader-request] argument as a [`loader`][loader].
111+
112+
### `serverLoader`
113+
114+
`serverLoader` is an asynchronous function to get the data from the server `loader` for this route. On client-side navigations, this will make a [fetch][fetch] call to the Remix server loader. If you opt-into running your `clientLoader` on hydration, then this function will return you the data that was already loaded on the server (via `Promise.resolve`).
115+
116+
[loader]: ./loader
117+
[loader-params]: ./loader#params
118+
[loader-request]: ./loader#request
119+
[hydratefallback]: ./hydrate-fallback
120+
[bff]: ../guides/bff
121+
[fetch]: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API

docs/route/hydrate-fallback.md

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
---
2+
title: HydrateFallback
3+
---
4+
5+
# `HydrateFallback`
6+
7+
<docs-info>The `HydrateFallback` component is only relevant when you are also setting [`clientLoader.hydrate=true`][hydrate-true] on a given route.</docs-info>
8+
9+
When provided, a `HydrateFallback` component will be rendered during SSR instead of your default route component, because you need to run your `clientLoader` to get a complete set of loader data. The `clientLoader` will then be called on hydration and once completed, Remix will render your route component with the complete loader data.
10+
11+
The most common use-case for this is augmenting your server data with client-side data, such as saved user preferences:
12+
13+
```tsx
14+
export async function loader() {
15+
const data = getServerData();
16+
return json(data);
17+
}
18+
19+
export async function clientLoader({
20+
request,
21+
params,
22+
serverLoader,
23+
}: ClientLoaderFunctionArgs) {
24+
const [serverData, preferences] = await Promise.all([
25+
serverLoader(),
26+
getUserPreferences(),
27+
]);
28+
return {
29+
...serverData,
30+
preferences,
31+
};
32+
}
33+
clientLoader.hydrate = true;
34+
35+
export function HydrateFallback() {
36+
return <p>Loading user preferences...</p>;
37+
}
38+
39+
export default function Component() {
40+
const data = useLoaderData<typeof clientLoader>();
41+
if (data.preferences.display === "list") {
42+
return <ListView items={data.items} />;
43+
} else {
44+
return <GridView items={data.items} />;
45+
}
46+
}
47+
```
48+
49+
If you have multiple routes with `clientLoader.hydrate=true`, then Remix will server-render up until the highest-discovered `HydrateFallback`. You cannot render an `<Outlet/>` in a `HydrateFallback` because children routes can't be guaranteed to operate correctly since their ancestor loader data may not yet be available if they are running `clientLoader` functions on hydration (i.e., use cases such as `useRouteLoaderData()` or `useMatches()`).
50+
51+
[hydrate-true]: ./client-loader#clientloaderhydrate

docs/route/loader.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ title: loader
66

77
<docs-success>Watch the <a href="https://www.youtube.com/playlist?list=PLXoynULbYuEDG2wBFSZ66b85EIspy3fy6">📼 Remix Single</a>: <a href="https://www.youtube.com/watch?v=NXqEP_PsPNc&list=PLXoynULbYuEDG2wBFSZ66b85EIspy3fy6">Loading data into components</a></docs-success>
88

9-
Each route can define a "loader" function that provides data to the route when rendering.
9+
Each route can define a `loader` function that provides data to the route when rendering.
1010

1111
```tsx
1212
import { json } from "@remix-run/node"; // or cloudflare/deno

0 commit comments

Comments
 (0)