-
Notifications
You must be signed in to change notification settings - Fork 13
Description
Data fetching is a perennial open question for us and I thought I'd write down some notes. I'm starting to run into this more and more as we try to use data prefetched in RR loaders in more places, for example for gating UI based on the user's role (e.g., #1232 and #1244). I haven't done a great job below of setting up the basics — what we're doing with RR and RQ and why — because @zephraph gets it and there's not really anyone else I need to explain it to. The best intro is probably React Query meets React Router, the post by the RQ maintainer.
Some relevant PRs: #1101 #1125 #1164 #1232
Using RQ and loader together has a lot of advantages, but it’s not as easy as it could be to do correctly. If you get the prefetch right, the query will always already be loaded, but there's no way to know statically whether you’re on a route that guarantees the prefetch has happened. If instead of prefetching and querying, you useLoaderData
directly, you know that it's there. Well… as long as you make sure you’re loading the data of a route you’re actually on. So we might have the problem no matter what.
We could consider dropping RQ and using RR loaders only, but RQ does give us a lot:
- Cache/SWR
- Dedupe simultaneous queries
- Refetch interval
On top of that, like I said above, dropping RQ doesn’t really solve anything, because the root of the problem is that useLoaderData
and useRouteLoaderData
return unknown
and any type you cast them to is a hopeful guess.
Dropping loaders and only using RQ is also not an option because loaders eliminate loading states for tons of things. Without a loader prefetching the system policy, for example, we would have to have a loading spinner either on the entire page or in just the header while we decide whether the user can see the silo/system picker. Both of these are terrible.
So for now the plan remains to figure out how to make it work with both together. We will probably need a coordination layer to make its use less error prone. expectSimultaneous
is not going to cut it.
A tip from the RQ maintainer
The maintainer of RQ has a great blog post React Query meets React Router on how to use RR loaders and RQ together and mentions this exact problem.
He suggests pulling the loader data to pass as initialData
to the query (example below). That eliminates the possibility that the query will return undefined while it’s loading. Maybe we can wrap up certain calls into a combined hook that calls useLoaderData
(or useRouteLoaderData
) and passes the result to a useQuery
with initialData
. Ultimately you still have the problem I keep mentioning of making sure you're calling useLoaderData
correctly so you actually get the data you expect.
Making it less error prone
useRouteLoaderData
and useLoaderData
don’t have typeof loader
generics like they do in Remix yet — they put this off until 6.5. They return unknown
, so no matter what, you’re doing a cast with the result.
const { projects: initialData } = useRouteLoaderData('route-id')
as Awaited<<ReturnType<typeof loader>>
const orgParams = useOrgParams()
const { data: projects } = useApiQuery(
'projectList',
{ path: orgParams },
{ initialData },
)
This is messy and too easy to get wrong. It’s not that bad if the loader and the component fetching the data are in the same file, which is hopefully true most of the time. It’s a lot worse if you’re looking at stuff from a faraway loader, like the user data. So for that one, we would write a hook that imports that stuff directly and does the cast, like this
/** Loader for the `<Route>` that wraps all authenticated routes. */
export const userLoader = async () => {
const [user, groups, systemPolicy] = await Promise.all([
apiQueryClient.fetchQuery('sessionMe', {}),
apiQueryClient.fetchQuery('sessionMeGroups', {}),
apiQueryClient.fetchQuery('systemPolicyView', {}),
])
return { user, groups, systemPolicy }
}
const USER_DATA_ROUTE_ID = 'user-data'
function useUserData() {
return useRouteLoaderData(USER_DATA_ROUTE_ID) as Awaited<ReturnType<typeof userLoader>>
}
This does get the right type:
You still have to make sure you’re calling it from a route that’s under the user data one, but in this case at least, that’s easy because almost every route is under there. Changing from prefetchQuery
to fetchQuery
means errors get thrown and bubble up the tree, but that’s probably a good thing anyway.