We've settled on React Suspense combined with the Async component from React Router as our preferred way of loading data for each screen. In general, data should then be passed in to components using props. This lets us start the data loading process as early as possible and render different parts of a page as soon as the needed data is available.
Look at manageParticipants.tsx for an example showing:
- Nested suspense/async components.
- Full type safety.
- Components which need to await multiple promises.
Look at home.tsx for an example showing:
- Customized error handling.
- Multi-stage async loader (i.e. you need to await one promise before you start a second fetch).
- Create a loader using
makeLoader
fromreact-router-typesafe
, ormakeParticipantLoader
if the calls you are making require a participantId parameter.- The loader should set up needed promises and return an object with Promises stored on descriptively-named keys, wrapped in a call to
defer
fromreact-router-typesafe
. Don't await the promises in the loader! - If you need to await a promise to get an intermediate value, either use a separate async function that returns a promise (e.g.
getSharingCounts
in home.tsx) (preferred) or.then(...)
(OK for simple/short continuations).
- The loader should set up needed promises and return an object with Promises stored on descriptively-named keys, wrapped in a call to
- Call the hook from your screen-level component:
const data = useLoaderData<typeof loader>();
- Wrap parts of your component tree that need async-loaded data in
<Suspense>
components, and provide a fallback loading indicator.<Suspense fallback={<Loading />}>
- Inside that, use the AsyncTypesafe component from AwaitTypesafe.tsx.
- Provide the promise you need to wait for in the
resolve
prop. - As the child of AsyncTypesafe, you need to provide an arrow function that accepts that promise.
- See the outer AsyncTypesafe component in manageParticipants.tsx for an example of using
resolveAll
to wait for multiple promises from the loader.
- Provide the promise you need to wait for in the
- (Optional) Provide a custom error element so errors only affect part of the screen, not the whole screen.
- Normally, a loader error means the screen won't render - the nearest error boundary will be used.
- See home.tsx for an example of allowing part of the page to work even if some of the loader promises fail.
- (Optional) Use the
revalidator
to trigger reloading the data when you change it:const reloader = useRevalidator();
at the top of your component, andreloader.revalidate();
after you've saved changes.- Don't forget to await your save, otherwise the revalidator might reload data before your action has saved it!
- React Router can work out when to revalidate without being told, but that relies on us using the React Router form methods. We might be able to get this to work with React Hook Form but haven't looked into it yet.
- Make sure you're importing
defer
,makeLoader
, anduseLoaderData
fromreact-router-typesafe
, NOTreact-router-dom
. This will hopefully change if/whenreact-router-dom
adds typesafe loader support. - Make sure you're using
makeLoader
ormakeParticipantLoader
to create your loader function. - Make sure your AsyncTypesafe child function returns a single React child. If you have multiple top-level components to return, wrap them in a fragment:
<><div /><div /></>
. - You shouldn't need to provide explicit types anywhere. As long as you provide the right generic parameter to the
useLoaderData
hook, everything (including the type of the arrow function inside the AsyncTypesafe component) should be inferred.