๐บ๏ธ Client Data (clientLoader, clientAction) #7634
Replies: 22 comments 53 replies
-
I've sorely missed a utility to solve precisely this without having to rely on How will awaiting the loader in the clientLoader work if you are deferring parts of the loader? Will we be able to further await some of these deferred promises in a clientLoader? And if yes, how will that function? I can't really see a place where I would want the above-mentioned functionality, but I thought I'd mention it anyways. |
Beta Was this translation helpful? Give feedback.
-
This also supercedes #4950 |
Beta Was this translation helpful? Give feedback.
-
A lot of folks today prefer using export function clientLoader({ request, params, serverFetch }) {...}
export function clientAction({ request, params, serverFetch }) {...} I think this just boils down to whether we want to export |
Beta Was this translation helpful? Give feedback.
-
I really, really like this design. Also makes it easy to race server+client loads/actions where warranted, or to have client loaders/actions that depend on server loader/action data. |
Beta Was this translation helpful? Give feedback.
-
Suggestion: bare returns from export async function loader({request}) {
return defer({
apiKey: API_KEY, // not a Promise (gets sent immediately)
result: db.get('some-key'), // a Promise (gets deferred)
})
}
export async function clientLoader({loader}) {
// start the server loader if it wasn't yet:
const serverData = loader();
// now do some client-side loading *in parallel with the server loader*:
const cachedResult = await idb.get('some-key');
// if we `await` the server loader data, it'll be available on first render.
return {cachedResult, ...(await serverData)};
}
export default function MyComponent() {
const data = useLoaderData();
data.cachedResult; // {"some":"data from idb, loaded on the client"}
data.apiKey; // "API_KEY from server loader"
data.result; // Promise that will resolve to the result of `db.get('some-key')`
return <Await promise={data.result}>{result => result.foo}</Await>;
} This also provides a mechanism for clientLoader's to load things based on non-Deferred data from a server loader. Imagine the same code as above, but with this export async function clientLoader({loader}) {
// load the server data (but don't wait for Deferred data!):
const serverData = await loader();
// use that data to do client-side loading, still in parallel with Deferred server data:
const headers = {authorization: `Bearer ${serverData.apiKey}`};
const user = await (await fetch('/api/user', {headers})).json();
return {user, ...serverData};
} |
Beta Was this translation helpful? Give feedback.
-
Do we want a way to distinguish initial load versus subsequent loads? We may want to use the server data for the HTML request for perf, but then make a direct API request subsequently to bypass the Remix hop in a BFF setup. function clientLoader({ loader }) {
// ๐ค where would this come from?
let isInitialDocumentRequest = ???
if (isInitialDocumentRequest) {
return loader(); // This resolves immediately with the hydrated server data
} else {
return callApiDirectly()
}
} A module-scoped variable might work? Or you could hack somethig on @developit suggested Object.defineProperty(req, 'mode', {
configurable: true,
enumerable: true,
get:() => 'navigate'
}); |
Beta Was this translation helpful? Give feedback.
-
Could It would also then be very useful to receive a flag in the clientLoader that this is a prefetch so that different caching or data loading strategies can be used. export async function clientLoader({ mode /* initial | prefetch | navigation */ }) {
if (mode === "prefetch") {
// TanStack Query prefetching example primes the cache
queryClient.prefetchQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
});
} else {
// A normal or initial navigation waits for the data, but maybe it's cached!
return await queryClient.fetchQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
});
}
} Related: https://tanstack.com/router/v1/docs/guide/preloading#preloading-with-data-loaders |
Beta Was this translation helpful? Give feedback.
-
I think folks have also asked in the past for the ability to add custom headers to the export function clientLoader({ request, params, loader }) {
return loader({
headers: {
'X-Custom': 'whatever'
}
});
} |
Beta Was this translation helpful? Give feedback.
-
First thought/concern for me is content layout shift. This API makes it easier to do user preferences that affect rendering (like theme and timezones) on the client then the server. The easier thing is what people will do. How could we discourage CLS with this API? |
Beta Was this translation helpful? Give feedback.
-
Great addition! Just a quick concern -- consider what could happen if you forget to destructure -export async function clientLoader({ request, params, loader }) {
+export async function clientLoader({ request, params }) {
let permissions = await loader();
let prefs = localStorage.getItem("prefs");
return { permissions, prefs };
}
export async function loader() {
return db.getPermissions();
} Unless I'm missing something, this might get server code leaked to the browser without noticing. Maybe there should be a way to detect this and fail the build? Or maybe a job for eslint? |
Beta Was this translation helpful? Give feedback.
-
This wasn't noted in the original discussion description but this would also open up massive ease of use for native API's allowing PWA's to essentially become remix first class citizens! |
Beta Was this translation helpful? Give feedback.
-
You mention IndexedDB in the proposal, which got me thinking about local-first, state synchronization, and realtime multiplayer. A stated goal of this proposal is to have the Remix paradigm cover more use-cases. Will there be some way to update clientLoader data outside of a navigation? Such as a websocket message, BroadcastChannel, etc.? What about revalidation? Naturally actions should revalidate all data, but I can think of situations where only clientLoaders should be reloaded. For example, some third-party package updates some IndexedDB data. Perhaps an extension to |
Beta Was this translation helpful? Give feedback.
-
I have so many use cases for this, where I wish I was in react router territory. please let this become a reality! |
Beta Was this translation helpful? Give feedback.
-
Do I understand this part correctly, that if clientLoader is present, route will always suspend on initial document request? Will it be possible to skip clientLoader entirely on document requests, to prevent rendering suspense boundary? I would like to be able to use clientLoader as strictly optimization on client navigation (e.g. use custom cached data) and wouldn't want to sacrifice functionality of initial documents for that. |
Beta Was this translation helpful? Give feedback.
-
Could clientLoader and clientAction provide a way to handle network errors which occur when calling the server? Thinking of #7688 . It would be pretty sweet if the below just worked export function clientAction({ action }) {
try {
return action()
} catch (e) {
if (isNetworkError(e)) {
// handle network error here
} else {
throw e
}
}
} |
Beta Was this translation helpful? Give feedback.
-
what's the story of error handling? is it the same of loader and action? |
Beta Was this translation helpful? Give feedback.
-
I have a question: How can we have a "typesafe" useLoaderData() in cases where the loader is only executed on initial loading, and the clientLoader is only executed on client-side navigations? I imagine something like: export const loader = /* ... */
// client data is forced to match loader data type
export const clientLoader: ClientDataFunction<typeof loader> = async ({ serverLoader }) => {
if(initialLoading) {
const data = await serverLoader(); // inferred loader data type
return data
}
return dataLoadedClientSide()
}
useLoaderData<typeof clientLoader>() |
Beta Was this translation helpful? Give feedback.
-
we can do similar stuff like PPR with this. Put all static data in loader, and all dynamic data in clientLoader, and we can pre-render the static part, |
Beta Was this translation helpful? Give feedback.
-
oh wow, I was thinking about this a lot recently. Why is browser storage a second-class citizen that requires escape hatches like useEffect? I love this! |
Beta Was this translation helpful? Give feedback.
-
About to dig into the source, but immediate hurdle I'm hitting is not having access to global context. We're forced to use a root level loader to pass in various levels of config (e.g. a cookie header). That's fine, but I don't seem to have any way to take that root loader context and pass it into child clientLoader's. This is the same rough prroblem that exists today with the top level ErrorBoundary as well. |
Beta Was this translation helpful? Give feedback.
-
Another issue I've hit, the need for more isomorphic utilities. For example, the json() bit is solvable in clientLoader vs loader (albeit kind of silly to force differentiation), but what about things like a redirect? Example common use case would be user sessions. They're hitting a route which requires auth. I want to avoid the server hop entirely, and send them to the login route on the cilent side when auth isnt present. |
Beta Was this translation helpful? Give feedback.
-
If a root has a server loader and a client loader with hydrate set to true, will navigation to the root be instant or will the navigation wait for the server loader to respond? (Assuming the client loader doesn't await the server loader, it just passes along the deferred server loader result as a promise) |
Beta Was this translation helpful? Give feedback.
-
Client Data
clientLoader
andclientAction
ย #5070Background
Proposal
Add support for
clientLoader
andclientAction
in route module exports:Client Loaders
Client loaders are just like React Router loaders receiving the
request
andparams
for the next location. They also have aloader
function that they can call to get the data from the server. This enables a client loader to be able to return data from the server, the client, or both.Behavior
loader
for the server loader to be called. Original document request will callloader
and the initial data will be returned fromloader()
, nearest suspense boundary will render for async client loadersClient Actions
Similarly to loaders, routes can export a
clientAction
function that will be called instead of the server action.Behavior
action
will throw. All data revalidatedFor document POST requests (
<Form reloadDocument method="post" />
), the client action is completely ignored.Benefits
Apps can now use a combination of persistent server and client data without needing to use a second paradigm outside of Remix, or skipping Remix and building client-side only with React Router.
This also creates fluid migration paths from React Router to Remix (and in the future, RSC) for in the Remix SPA proposal.
Beta Was this translation helpful? Give feedback.
All reactions