From a67562224ce69521ae3bb01677a3f648dfb039b2 Mon Sep 17 00:00:00 2001 From: "Kent C. Dodds" Date: Thu, 14 Jul 2022 15:12:57 -0600 Subject: [PATCH 1/5] add deferred docs --- docs/components/deferred.md | 51 ++++++++ docs/fetch/deferred.md | 18 +++ docs/guides/deferred.md | 201 ++++++++++++++++++++++++++++++++ docs/hooks/use-deferred-data.md | 32 +++++ 4 files changed, 302 insertions(+) create mode 100644 docs/components/deferred.md create mode 100644 docs/fetch/deferred.md create mode 100644 docs/guides/deferred.md create mode 100644 docs/hooks/use-deferred-data.md diff --git a/docs/components/deferred.md b/docs/components/deferred.md new file mode 100644 index 0000000000..8767f180ad --- /dev/null +++ b/docs/components/deferred.md @@ -0,0 +1,51 @@ +--- +title: Deferred +new: true +--- + +## `` + +
+ Type declaration + +```tsx +export declare function Deferred({ + children, + value, + fallback, + errorElement, +}: DeferredProps): JSX.Element; +``` + +
+ +This component is responsible for resolving deferred values accessed from [`useLoaderData`][useloaderdata]. This can be thought of as an auto-suspending React `` component and an error boundary all in one. + +`` can be used to resolve the deferred value in one of two ways: + +Directly as a render function: + +```tsx + + {(data) =>

{data}

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

{value}

; +} + + + +; +``` + +`` is paired with [`deferred()`][deferred response] in 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]. + +[useloaderdata]: ../hooks/use-loader-data +[deferred response]: ../fetch/deferred +[deferred guide]: ../guides/deferred diff --git a/docs/fetch/deferred.md b/docs/fetch/deferred.md new file mode 100644 index 0000000000..92ce64dd07 --- /dev/null +++ b/docs/fetch/deferred.md @@ -0,0 +1,18 @@ +--- +title: deferred +--- + +
+ Type declaration + +```tsx +export declare function deferred( + 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..543e8ca3ad --- /dev/null +++ b/docs/guides/deferred.md @@ -0,0 +1,201 @@ +--- +title: Deferred +description: When, why, and how to defer non-critical data loading with React 18 and React Router's deferred API. +--- + +# Deferred + +## 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 [`deferred` Response][deferred response] utility and [``][deferred] component / [`useDeferredData`][usedeferreddata] 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 `deferred` + +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 +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 }); +} + +export default function PackageRoute() { + const data = useLoaderData(); + + return ( +
+

Let's locate your package

+ Loading package location...

} + errorElement={ +

Error loading package location!

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

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

+ )} +
+
+ ); +} +``` + +
+ Alternatively, you can use the `useDeferredData` 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 +export default function PackageRoute() { + const data = useLoaderData(); + + return ( +
+

Let's locate your package

+ Loading package location...

} + errorElement={ +

Error loading package location!

+ } + > + +
+
+ ); +} + +function PackageLocation() { + const packageLocation = useDeferredData(); + 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 derferred or not based on whether you include the `await` keyword: + +```tsx +return deferred({ + // 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 deferred({ + 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 fallbackElement render? + +The `` `fallbackElement` prop only renders on the initial render of the `` component. It will not 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 `deferred`, 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 +[deferred response]: ../fetch/deferred +[deferred]: ../components/deferred +[usedeferreddata]: ../hooks/use-deferred-data diff --git a/docs/hooks/use-deferred-data.md b/docs/hooks/use-deferred-data.md new file mode 100644 index 0000000000..2eff5b0148 --- /dev/null +++ b/docs/hooks/use-deferred-data.md @@ -0,0 +1,32 @@ +--- +title: useDeferredData +new: true +--- + +# `useDeferredData` + +
+ Type declaration + +```tsx +export declare function useDeferredData< + Data +>(): ResolvedDeferrable; +``` + +
+ +```tsx +function Accessor() { + const value = useDeferredData(); + return

{value}

; +} + + + +; +``` + +This hook returns the resolved data from the nearest `` component. See the [`` docs][deferred docs] for more information. + +[deferred docs]: ../components/deferred From 6859c8f332455a6d75836cce0eb4c08ef1a37ee3 Mon Sep 17 00:00:00 2001 From: "Kent C. Dodds" Date: Thu, 14 Jul 2022 15:30:32 -0600 Subject: [PATCH 2/5] improve docs --- docs/fetch/deferred.md | 2 ++ docs/guides/deferred.md | 14 ++++++++------ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/docs/fetch/deferred.md b/docs/fetch/deferred.md index 92ce64dd07..e13b48443d 100644 --- a/docs/fetch/deferred.md +++ b/docs/fetch/deferred.md @@ -2,6 +2,8 @@ title: deferred --- +# `deferred` +
Type declaration diff --git a/docs/guides/deferred.md b/docs/guides/deferred.md index 543e8ca3ad..b9f6f16a74 100644 --- a/docs/guides/deferred.md +++ b/docs/guides/deferred.md @@ -3,7 +3,7 @@ title: Deferred description: When, why, and how to defer non-critical data loading with React 18 and React Router's deferred API. --- -# Deferred +# Deferred Guide ## The problem @@ -64,16 +64,18 @@ Let's take a dive into how to accomplish this. 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 -import { json, useLoaderData } from "react-router-dom"; +```jsx lines=[1,5,10,20-33] +import { deferred, useLoaderData } from "react-router-dom"; import { getPackageLocation } from "./api/packages"; async function loader({ params }) { - const packageLocation = await getPackageLocation( + const packageLocationPromise = getPackageLocation( params.packageId ); - return json({ packageLocation }); + return deferred({ + packageLocation: packageLocationPromise, + }); } export default function PackageRoute() { @@ -106,7 +108,7 @@ export default function PackageRoute() { 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 +```jsx lines=[21] export default function PackageRoute() { const data = useLoaderData(); From 70b46b94fca095bb6bf886a494b4171f4211904b Mon Sep 17 00:00:00 2001 From: "Kent C. Dodds" Date: Fri, 15 Jul 2022 16:46:38 -0600 Subject: [PATCH 3/5] Update docs/components/deferred.md Co-authored-by: Matt Brophy --- docs/components/deferred.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/components/deferred.md b/docs/components/deferred.md index 8767f180ad..b1cf7339c2 100644 --- a/docs/components/deferred.md +++ b/docs/components/deferred.md @@ -9,6 +9,26 @@ new: true Type declaration ```tsx +export type Deferrable = never | T | Promise; +export type ResolvedDeferrable = T extends null | undefined + ? T + : T extends Deferrable + ? T2 extends Promise + ? T3 + : T2 + : T; + +export interface DeferredResolveRenderFunction { + (data: ResolvedDeferrable): JSX.Element; +} + +export interface DeferredProps { + children: React.ReactNode | DeferredResolveRenderFunction; + value: Data; + fallbackElement: React.SuspenseProps["fallback"]; + errorElement?: React.ReactNode; +} + export declare function Deferred({ children, value, From 005a035ab4c89ec4871b5925a7c7c87e8b2bb65a Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Tue, 26 Jul 2022 17:00:34 -0400 Subject: [PATCH 4/5] Update docs for new await naming --- docs/components/await.md | 60 ++++++++++++++++++++++++++++ docs/components/deferred.md | 71 --------------------------------- docs/guides/deferred.md | 66 ++++++++++++++++-------------- docs/hooks/use-awaited-data.md | 30 ++++++++++++++ docs/hooks/use-deferred-data.md | 32 --------------- 5 files changed, 126 insertions(+), 133 deletions(-) create mode 100644 docs/components/await.md delete mode 100644 docs/components/deferred.md create mode 100644 docs/hooks/use-awaited-data.md delete mode 100644 docs/hooks/use-deferred-data.md diff --git a/docs/components/await.md b/docs/components/await.md new file mode 100644 index 0000000000..43e621d134 --- /dev/null +++ b/docs/components/await.md @@ -0,0 +1,60 @@ +--- +title: Await +new: true +--- + +## `` + +
+ Type declaration + +```tsx +export interface AwaitResolveRenderFunction { + (data: Awaited): JSX.Element; +} + +export interface AwaitProps { + children: React.ReactNode | AwaitResolveRenderFunction; + errorElement?: React.ReactNode; + promise: DeferredPromise; +} + +export declare function Await({ + children, + errorElement, + promise, +}: AwaitProps): JSX.Element; +``` + +
+ +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 `useAwaitedData` hook: + +```tsx +function Accessor() { + const data = useAwaitedData(); + return

{data}

; +} + + + +; +``` + +`` is primarily intended to be used with the [`deferred()`][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]. + +[useloaderdata]: ../hooks/use-loader-data +[deferred response]: ../fetch/deferred +[deferred guide]: ../guides/deferred diff --git a/docs/components/deferred.md b/docs/components/deferred.md deleted file mode 100644 index b1cf7339c2..0000000000 --- a/docs/components/deferred.md +++ /dev/null @@ -1,71 +0,0 @@ ---- -title: Deferred -new: true ---- - -## `` - -
- Type declaration - -```tsx -export type Deferrable = never | T | Promise; -export type ResolvedDeferrable = T extends null | undefined - ? T - : T extends Deferrable - ? T2 extends Promise - ? T3 - : T2 - : T; - -export interface DeferredResolveRenderFunction { - (data: ResolvedDeferrable): JSX.Element; -} - -export interface DeferredProps { - children: React.ReactNode | DeferredResolveRenderFunction; - value: Data; - fallbackElement: React.SuspenseProps["fallback"]; - errorElement?: React.ReactNode; -} - -export declare function Deferred({ - children, - value, - fallback, - errorElement, -}: DeferredProps): JSX.Element; -``` - -
- -This component is responsible for resolving deferred values accessed from [`useLoaderData`][useloaderdata]. This can be thought of as an auto-suspending React `` component and an error boundary all in one. - -`` can be used to resolve the deferred value in one of two ways: - -Directly as a render function: - -```tsx - - {(data) =>

{data}

} -
-``` - -Or indirectly via the `useDeferredData` hook: - -```tsx -function Accessor() { - const value = useDeferredData(); - return

{value}

; -} - - - -; -``` - -`` is paired with [`deferred()`][deferred response] in 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]. - -[useloaderdata]: ../hooks/use-loader-data -[deferred response]: ../fetch/deferred -[deferred guide]: ../guides/deferred diff --git a/docs/guides/deferred.md b/docs/guides/deferred.md index b9f6f16a74..b6c74792a8 100644 --- a/docs/guides/deferred.md +++ b/docs/guides/deferred.md @@ -53,7 +53,7 @@ But it's still sub optimal in most cases (especially if you're code-splitting ro ## The solution -React Router takes advantage of React 18's Suspense for data fetching using the [`deferred` Response][deferred response] utility and [``][deferred] component / [`useDeferredData`][usedeferreddata] hook. By using these APIs, you can solve both of these problems: +React Router takes advantage of React 18's Suspense for data fetching using the [`deferred` Response][deferred response] utility and [``][await] component / [`useAwaitedData`][useawaiteddata] 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 @@ -62,7 +62,7 @@ Let's take a dive into how to accomplish this. ### Using `deferred` -Start by adding `` for your slow data requests where you'd rather render a fallback UI. Let's do that for our example above: +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 { deferred, useLoaderData } from "react-router-dom"; @@ -84,27 +84,30 @@ export default function PackageRoute() { return (

Let's locate your package

- Loading package location...

} - errorElement={ -

Error loading package location!

- } + Loading package location...

} > - {(packageLocation) => ( -

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

- )} -
+ Error loading package location!

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

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

+ )} +
+
); } ```
- Alternatively, you can use the `useDeferredData` hook: + Alternatively, you can use the `useAwaitedData` 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: @@ -115,21 +118,24 @@ export default function PackageRoute() { return (

Let's locate your package

- Loading package location...

} - errorElement={ -

Error loading package location!

- } + Loading package location...

} > - -
+ Error loading package location!

+ } + > + +
+
); } function PackageLocation() { - const packageLocation = useDeferredData(); + const packageLocation = useAwaitedData(); return (

Your package is at {packageLocation.latitude} lat and{" "} @@ -145,7 +151,7 @@ function PackageLocation() { 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 derferred or not based on whether you include the `await` keyword: +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 deferred({ @@ -186,9 +192,9 @@ The React Router defer API is another lever React Router offers to give you a ni 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 fallbackElement render? +### When does the `` fallback render? -The `` `fallbackElement` prop only renders on the initial render of the `` component. It will not 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). +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. @@ -199,5 +205,5 @@ So just keep this in mind: **Deferred is 100% only about the initial load of a r [link]: ../components/link [usefetcher]: ../hooks/use-fetcher [deferred response]: ../fetch/deferred -[deferred]: ../components/deferred -[usedeferreddata]: ../hooks/use-deferred-data +[await]: ../components/await +[useawaiteddata]: ../hooks/use-awaited-data diff --git a/docs/hooks/use-awaited-data.md b/docs/hooks/use-awaited-data.md new file mode 100644 index 0000000000..ae9cbc4ff8 --- /dev/null +++ b/docs/hooks/use-awaited-data.md @@ -0,0 +1,30 @@ +--- +title: useAwaitedData +new: true +--- + +# `useAwaitedData` + +

+ Type declaration + +```tsx +export declare function useAwaitedData(): unknown; +``` + +
+ +```tsx +function Accessor() { + const data = useAwaitedData(); + 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/docs/hooks/use-deferred-data.md b/docs/hooks/use-deferred-data.md deleted file mode 100644 index 2eff5b0148..0000000000 --- a/docs/hooks/use-deferred-data.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -title: useDeferredData -new: true ---- - -# `useDeferredData` - -
- Type declaration - -```tsx -export declare function useDeferredData< - Data ->(): ResolvedDeferrable; -``` - -
- -```tsx -function Accessor() { - const value = useDeferredData(); - return

{value}

; -} - - - -; -``` - -This hook returns the resolved data from the nearest `` component. See the [`` docs][deferred docs] for more information. - -[deferred docs]: ../components/deferred From 97766e788babb55778933e12429a68ffda5e55ab Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Mon, 8 Aug 2022 14:32:12 -0400 Subject: [PATCH 5/5] Update based on latest naming updates --- docs/components/await.md | 52 ++++++++++++------- docs/fetch/{deferred.md => defer.md} | 8 +-- docs/guides/deferred.md | 32 ++++++------ docs/hooks/use-async-error.md | 37 +++++++++++++ ...use-awaited-data.md => use-async-value.md} | 8 +-- packages/react-router/lib/components.tsx | 2 +- 6 files changed, 95 insertions(+), 44 deletions(-) rename docs/fetch/{deferred.md => defer.md} (74%) create mode 100644 docs/hooks/use-async-error.md rename docs/hooks/{use-awaited-data.md => use-async-value.md} (74%) diff --git a/docs/components/await.md b/docs/components/await.md index 43e621d134..0cdd1b5aeb 100644 --- a/docs/components/await.md +++ b/docs/components/await.md @@ -9,21 +9,19 @@ new: true Type declaration ```tsx -export interface AwaitResolveRenderFunction { - (data: Awaited): JSX.Element; -} +declare function Await( + props: AwaitProps +): React.ReactElement; -export interface AwaitProps { +interface AwaitProps { children: React.ReactNode | AwaitResolveRenderFunction; errorElement?: React.ReactNode; - promise: DeferredPromise; + resolve: TrackedPromise | any; } -export declare function Await({ - children, - errorElement, - promise, -}: AwaitProps): JSX.Element; +interface AwaitResolveRenderFunction { + (data: Awaited): React.ReactElement; +} ```
@@ -35,26 +33,42 @@ This component is responsible for rendering Promises. This can be thought of as Directly as a render function: ```tsx - - {(data) =>

{data}

} -
+{(data) =>

{data}

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

{data}

; } - + -
; +
; ``` -`` is primarily intended to be used with the [`deferred()`][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]. +`` 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 -[deferred response]: ../fetch/deferred +[userouteerror]: ../hooks/use-route-error +[defer response]: ../fetch/defer [deferred guide]: ../guides/deferred diff --git a/docs/fetch/deferred.md b/docs/fetch/defer.md similarity index 74% rename from docs/fetch/deferred.md rename to docs/fetch/defer.md index e13b48443d..d62b643806 100644 --- a/docs/fetch/deferred.md +++ b/docs/fetch/defer.md @@ -1,15 +1,15 @@ --- -title: deferred +title: defer --- -# `deferred` +# `defer`
Type declaration ```tsx -export declare function deferred( - data: Record +declare function defer( + data: Record ): DeferredData; ``` diff --git a/docs/guides/deferred.md b/docs/guides/deferred.md index b6c74792a8..c4542787e5 100644 --- a/docs/guides/deferred.md +++ b/docs/guides/deferred.md @@ -1,9 +1,9 @@ --- -title: Deferred -description: When, why, and how to defer non-critical data loading with React 18 and React Router's deferred API. +title: Deferred Data +description: When, why, and how to defer non-critical data loading with React 18 and React Router's defer API. --- -# Deferred Guide +# Deferred Data Guide ## The problem @@ -53,19 +53,19 @@ But it's still sub optimal in most cases (especially if you're code-splitting ro ## The solution -React Router takes advantage of React 18's Suspense for data fetching using the [`deferred` Response][deferred response] utility and [``][await] component / [`useAwaitedData`][useawaiteddata] hook. By using these APIs, you can solve both of these problems: +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 `deferred` +### 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 { deferred, useLoaderData } from "react-router-dom"; +import { defer, useLoaderData } from "react-router-dom"; import { getPackageLocation } from "./api/packages"; async function loader({ params }) { @@ -73,7 +73,7 @@ async function loader({ params }) { params.packageId ); - return deferred({ + return defer({ packageLocation: packageLocationPromise, }); } @@ -88,7 +88,7 @@ export default function PackageRoute() { fallback={

Loading package location...

} > Error loading package location!

} @@ -107,7 +107,7 @@ export default function PackageRoute() { ```
- Alternatively, you can use the `useAwaitedData` hook: + 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: @@ -122,7 +122,7 @@ export default function PackageRoute() { fallback={

Loading package location...

} > Error loading package location!

} @@ -135,7 +135,7 @@ export default function PackageRoute() { } function PackageLocation() { - const packageLocation = useAwaitedData(); + const packageLocation = useAsyncValue(); return (

Your package is at {packageLocation.latitude} lat and{" "} @@ -154,7 +154,7 @@ So rather than waiting for the component before we can trigger the fetch request 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 deferred({ +return defer({ // not deferred: packageLocation: await packageLocationPromise, // deferred: @@ -174,7 +174,7 @@ async function loader({ request, params }) { params.packageId ); - return deferred({ + return defer({ packageLocation: shouldDefer ? packageLocationPromise : await packageLocationPromise, @@ -198,12 +198,12 @@ The `` component will only throw the promise up the `` bounda 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 `deferred`, 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. +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 -[deferred response]: ../fetch/deferred +[defer response]: ../fetch/defer [await]: ../components/await -[useawaiteddata]: ../hooks/use-awaited-data +[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-awaited-data.md b/docs/hooks/use-async-value.md similarity index 74% rename from docs/hooks/use-awaited-data.md rename to docs/hooks/use-async-value.md index ae9cbc4ff8..00384fe254 100644 --- a/docs/hooks/use-awaited-data.md +++ b/docs/hooks/use-async-value.md @@ -1,22 +1,22 @@ --- -title: useAwaitedData +title: useAsyncValue new: true --- -# `useAwaitedData` +# `useAsyncValue`
Type declaration ```tsx -export declare function useAwaitedData(): unknown; +export declare function useAsyncValue(): unknown; ```
```tsx function Accessor() { - const data = useAwaitedData(); + const data = useAsyncValue(); return

{data}

; } 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 {