diff --git a/docs/components/await.md b/docs/components/await.md new file mode 100644 index 0000000000..0cdd1b5aeb --- /dev/null +++ b/docs/components/await.md @@ -0,0 +1,74 @@ +--- +title: Await +new: true +--- + +## `` + +
+ Type declaration + +```tsx +declare function Await( + props: AwaitProps +): React.ReactElement; + +interface AwaitProps { + children: React.ReactNode | AwaitResolveRenderFunction; + errorElement?: React.ReactNode; + resolve: TrackedPromise | any; +} + +interface AwaitResolveRenderFunction { + (data: Awaited): React.ReactElement; +} +``` + +
+ +This component is responsible for rendering Promises. This can be thought of as a Promise-renderer with a built-in error boundary. You should always render `` inside a `` boundary to handle fallback displays prior to the promise settling. + +`` can be used to resolve the promise in one of two ways: + +Directly as a render function: + +```tsx +{(data) =>

{data}

}
+``` + +Or indirectly via the `useAsyncValue` hook: + +```tsx +function Accessor() { + const data = useAsyncValue(); + return

{data}

; +} + + + +; +``` + +`` is primarily intended to be used with the [`defer()`][deferred response] data returned from your `loader`. Returning a deferred value from your loader will allow you to render fallbacks with ``. A full example can be found in the [Deferred guide][deferred guide]. + +### Error Handling + +If the passed promise rejects, you can provide an optional `errorElement` to handle that error in a contextual UI via the `useAsyncError` hook. If you do not provide an errorElement, the rejected value will bubble up to the nearest route-level `errorElement` and be accessible via the [`useRouteError`][userouteerror] hook. + +```tsx +function ErrorHandler() { + const error = useAsyncError(); + return ( +

Uh Oh, something went wrong! {error.message}

+ ); +} + +}> + +; +``` + +[useloaderdata]: ../hooks/use-loader-data +[userouteerror]: ../hooks/use-route-error +[defer response]: ../fetch/defer +[deferred guide]: ../guides/deferred diff --git a/docs/fetch/defer.md b/docs/fetch/defer.md new file mode 100644 index 0000000000..d62b643806 --- /dev/null +++ b/docs/fetch/defer.md @@ -0,0 +1,20 @@ +--- +title: defer +--- + +# `defer` + +
+ Type declaration + +```tsx +declare function defer( + data: Record +): DeferredData; +``` + +
+ +This utility allows you to defer certain parts of your loader. See the [Deferred guide][deferred guide] for more information. + +[deferred guide]: ../guides/deferred diff --git a/docs/guides/deferred.md b/docs/guides/deferred.md new file mode 100644 index 0000000000..c4542787e5 --- /dev/null +++ b/docs/guides/deferred.md @@ -0,0 +1,209 @@ +--- +title: Deferred Data +description: When, why, and how to defer non-critical data loading with React 18 and React Router's defer API. +--- + +# Deferred Data Guide + +## The problem + +Imagine a scenario where one of your routes' loaders needs to retrieve some data that for one reason or another is quite slow. For example, let's say you're showing the user the location of a package that's being delivered to their home: + +```jsx +import { json, useLoaderData } from "react-router-dom"; +import { getPackageLocation } from "./api/packages"; + +async function loader({ params }) { + const packageLocation = await getPackageLocation( + params.packageId + ); + + return json({ packageLocation }); +} + +function PackageRoute() { + const data = useLoaderData(); + const { packageLocation } = data; + + return ( +
+

Let's locate your package

+

+ Your package is at {packageLocation.latitude} lat + and {packageLocation.longitude} long. +

+
+ ); +} +``` + +We'll assume that `getPackageLocation` is slow. This will lead to initial page load times and transitions to that route to take as long as the slowest bit of data. There are a few things you can do to optimize this and improve the user experience: + +- Speed up the slow thing (😅). +- Parallelize data loading with `Promise.all` (we have nothing to parallelize in our example, but it might help a bit in other situations). +- Add a global transition spinner (helps a bit with UX). +- Add a localized skeleton UI (helps a bit with UX). + +If these approaches don't work well, then you may feel forced to move the slow data out of the `loader` into a component fetch (and show a skeleton fallback UI while loading). In this case you'd render the fallback UI on mount and fire off the fetch for the data. This is actually not so terrible from a DX standpoint thanks to [`useFetcher`][usefetcher]. And from a UX standpoint this improves the loading experience for both client-side transitions as well as initial page load. So it does seem to solve the problem. + +But it's still sub optimal in most cases (especially if you're code-splitting route components) for two reasons: + +1. Client-side fetching puts your data request on a waterfall: document -> JavaScript -> Lazy Loaded Route -> data fetch +2. Your code can't easily switch between component fetching and route fetching (more on this later). + +## The solution + +React Router takes advantage of React 18's Suspense for data fetching using the [`defer` Response][defer response] utility and [``][await] component / [`useAsyncValue`][useasyncvalue] hook. By using these APIs, you can solve both of these problems: + +1. You're data is no longer on a waterfall: document -> JavaScript -> Lazy Loaded Route & data (in parallel) +2. Your can easily switch between rendering the fallback and waiting for the data + +Let's take a dive into how to accomplish this. + +### Using `defer` + +Start by adding `` for your slow data requests where you'd rather render a fallback UI. Let's do that for our example above: + +```jsx lines=[1,5,10,20-33] +import { defer, useLoaderData } from "react-router-dom"; +import { getPackageLocation } from "./api/packages"; + +async function loader({ params }) { + const packageLocationPromise = getPackageLocation( + params.packageId + ); + + return defer({ + packageLocation: packageLocationPromise, + }); +} + +export default function PackageRoute() { + const data = useLoaderData(); + + return ( +
+

Let's locate your package

+ Loading package location...

} + > + Error loading package location!

+ } + > + {(packageLocation) => ( +

+ Your package is at {packageLocation.latitude}{" "} + lat and {packageLocation.longitude} long. +

+ )} +
+
+
+ ); +} +``` + +
+ Alternatively, you can use the `useAsyncValue` hook: + +If you're not jazzed about bringing back render props, you can use a hook, but you'll have to break things out into another component: + +```jsx lines=[21] +export default function PackageRoute() { + const data = useLoaderData(); + + return ( +
+

Let's locate your package

+ Loading package location...

} + > + Error loading package location!

+ } + > + +
+
+
+ ); +} + +function PackageLocation() { + const packageLocation = useAsyncValue(); + return ( +

+ Your package is at {packageLocation.latitude} lat and{" "} + {packageLocation.longitude} long. +

+ ); +} +``` + +
+ +## Evaluating the solution + +So rather than waiting for the component before we can trigger the fetch request, we start the request for the slow data as soon as the user starts the transition to the new route. This can significantly speed up the user experience for slower networks. + +Additionally, the API that React Router exposes for this is extremely ergonomic. You can literally switch between whether something is going to be deferred or not based on whether you include the `await` keyword: + +```tsx +return defer({ + // not deferred: + packageLocation: await packageLocationPromise, + // deferred: + packageLocation: packageLocationPromise, +}); +``` + +Because of this, you can A/B test deferring, or even determine whether to defer based on the user or data being requested: + +```tsx +async function loader({ request, params }) { + const packageLocationPromise = getPackageLocation( + params.packageId + ); + const shouldDefer = shouldDeferPackageLocation( + request, + params.packageId + ); + + return defer({ + packageLocation: shouldDefer + ? packageLocationPromise + : await packageLocationPromise, + }); +} +``` + +That `shouldDeferPackageLocation` could be implemented to check the user making the request, whether the package location data is in a cache, the status of an A/B test, or whatever else you want. This is pretty sweet 🍭 + +## FAQ + +### Why not defer everything by default? + +The React Router defer API is another lever React Router offers to give you a nice way to choose between trade-offs. Do you want the page to render more quickly? Defer stuff. Do you want a lower CLS (Content Layout Shift)? Don't defer stuff. You want a faster render, but also want a lower CLS? Defer just the slow and unimportant stuff. + +It's all trade-offs, and what's neat about the API design is that it's well suited for you to do easy experimentation to see which trade-offs lead to better results for your real-world key indicators. + +### When does the `` fallback render? + +The `` component will only throw the promise up the `` boundary on the initial render of the `` component with an unsettled promise. It will not re-render the fallback if props change. Effectively, this means that you will not get a fallback rendered when a user submits a form and loader data is revalidated and you will not get a fallback rendered when the user navigates to the same route with different params (in the context of our above example, if the user selects from a list of packages on the left to find their location on the right). + +This may feel counter-intuitive at first, but stay with us, we really thought this through and it's important that it works this way. Let's imagine a world without the deferred API. For those scenarios you're probably going to want to implement Optimistic UI for form submissions/revalidation and some Pending UI for sibling route navigations. + +When you decide you'd like to try the trade-offs of `defer`, we don't want you to have to change or remove those optimizations because we want you to be able to easily switch between deferring some data and not deferring it. So we ensure that your existing pending states work the same way. If we didn't do this, then you could experience what we call "Popcorn UI" where submissions of data trigger the fallback loading state instead of the optimistic UI you'd worked hard on. + +So just keep this in mind: **Deferred is 100% only about the initial load of a route.** + +[link]: ../components/link +[usefetcher]: ../hooks/use-fetcher +[defer response]: ../fetch/defer +[await]: ../components/await +[useasyncvalue]: ../hooks/use-async-data diff --git a/docs/hooks/use-async-error.md b/docs/hooks/use-async-error.md new file mode 100644 index 0000000000..cbc94e32d0 --- /dev/null +++ b/docs/hooks/use-async-error.md @@ -0,0 +1,37 @@ +--- +title: useAsyncError +new: true +--- + +# `useAsyncError` + +
+ Type declaration + +```tsx +export declare function useAsyncError(): unknown; +``` + +
+ +```tsx +function Accessor() { + const data = useAsyncValue(); + return

{data}

; +} + +function ErrorHandler() { + const error = useAsyncError(); + return ( +

Uh Oh, something went wrong! {error.message}

+ ); +} + +}> + +; +``` + +This hook returns the rejection value from the nearest `` component. See the [`` docs][await docs] for more information. + +[await docs]: ../components/await diff --git a/docs/hooks/use-async-value.md b/docs/hooks/use-async-value.md new file mode 100644 index 0000000000..00384fe254 --- /dev/null +++ b/docs/hooks/use-async-value.md @@ -0,0 +1,30 @@ +--- +title: useAsyncValue +new: true +--- + +# `useAsyncValue` + +
+ Type declaration + +```tsx +export declare function useAsyncValue(): unknown; +``` + +
+ +```tsx +function Accessor() { + const data = useAsyncValue(); + return

{data}

; +} + + + +; +``` + +This hook returns the resolved data from the nearest `` component. See the [`` docs][await docs] for more information. + +[await docs]: ../components/await diff --git a/packages/react-router/lib/components.tsx b/packages/react-router/lib/components.tsx index 2fb08e44a4..dbe55b0d2c 100644 --- a/packages/react-router/lib/components.tsx +++ b/packages/react-router/lib/components.tsx @@ -440,7 +440,7 @@ export function Routes({ } export interface AwaitResolveRenderFunction { - (data: Awaited): JSX.Element; + (data: Awaited): React.ReactElement; } export interface AwaitProps {