Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add deferred docs #9061

Merged
merged 6 commits into from
Aug 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
74 changes: 74 additions & 0 deletions docs/components/await.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
---
title: Await
new: true
---

## `<Await />`

<details>
<summary>Type declaration</summary>

```tsx
declare function Await(
props: AwaitProps
): React.ReactElement;

interface AwaitProps {
children: React.ReactNode | AwaitResolveRenderFunction;
errorElement?: React.ReactNode;
resolve: TrackedPromise | any;
}

interface AwaitResolveRenderFunction {
(data: Awaited<any>): React.ReactElement;
}
```

</details>

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 `<Await>` inside a `<React.Suspense>` boundary to handle fallback displays prior to the promise settling.

`<Await>` can be used to resolve the promise in one of two ways:

Directly as a render function:

```tsx
<Await resolve={promise}>{(data) => <p>{data}</p>}</Await>
```

Or indirectly via the `useAsyncValue` hook:

```tsx
function Accessor() {
const data = useAsyncValue();
return <p>{data}</p>;
}

<Await resolve={promise}>
<Accessor />
</Await>;
```

`<Await>` 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 `<Await>`. 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 (
<p>Uh Oh, something went wrong! {error.message}</p>
);
}

<Await resolve={promise} errorElement={<ErrorElement />}>
<Accessor />
</Await>;
```

[useloaderdata]: ../hooks/use-loader-data
[userouteerror]: ../hooks/use-route-error
[defer response]: ../fetch/defer
[deferred guide]: ../guides/deferred
20 changes: 20 additions & 0 deletions docs/fetch/defer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
title: defer
---

# `defer`

<details>
<summary>Type declaration</summary>

```tsx
declare function defer(
data: Record<string, unknown>
): DeferredData;
```

</details>

This utility allows you to defer certain parts of your loader. See the [Deferred guide][deferred guide] for more information.

[deferred guide]: ../guides/deferred
209 changes: 209 additions & 0 deletions docs/guides/deferred.md
Original file line number Diff line number Diff line change
@@ -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 (
<main>
<h1>Let's locate your package</h1>
<p>
Your package is at {packageLocation.latitude} lat
and {packageLocation.longitude} long.
</p>
</main>
);
}
```

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 />`][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 `<Await />` 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 (
<main>
<h1>Let's locate your package</h1>
<React.Suspense
fallback={<p>Loading package location...</p>}
>
<Await
resolve={data.packageLocation}
errorElement={
<p>Error loading package location!</p>
}
>
{(packageLocation) => (
<p>
Your package is at {packageLocation.latitude}{" "}
lat and {packageLocation.longitude} long.
</p>
)}
</Await>
</React.Suspense>
</main>
);
}
```

<details>
<summary>Alternatively, you can use the `useAsyncValue` hook:</summary>

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 (
<main>
<h1>Let's locate your package</h1>
<React.Suspense
fallback={<p>Loading package location...</p>}
>
<Await
resolve={data.packageLocation}
errorElement={
<p>Error loading package location!</p>
}
>
<PackageLocation />
</Await>
</React.Suspense>
</main>
);
}

function PackageLocation() {
const packageLocation = useAsyncValue();
return (
<p>
Your package is at {packageLocation.latitude} lat and{" "}
{packageLocation.longitude} long.
</p>
);
}
```

</details>

## 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 `<Suspense/>` fallback render?

The `<Await />` component will only throw the promise up the `<Suspense>` boundary on the initial render of the `<Await />` 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
37 changes: 37 additions & 0 deletions docs/hooks/use-async-error.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
title: useAsyncError
new: true
---

# `useAsyncError`

<details>
<summary>Type declaration</summary>

```tsx
export declare function useAsyncError(): unknown;
```

</details>

```tsx
function Accessor() {
const data = useAsyncValue();
return <p>{data}</p>;
}

function ErrorHandler() {
const error = useAsyncError();
return (
<p>Uh Oh, something went wrong! {error.message}</p>
);
}

<Await resolve={promise} errorElement={<ErrorElement />}>
<Accessor />
</Await>;
```

This hook returns the rejection value from the nearest `<Await>` component. See the [`<Await>` docs][await docs] for more information.

[await docs]: ../components/await
30 changes: 30 additions & 0 deletions docs/hooks/use-async-value.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
title: useAsyncValue
new: true
---

# `useAsyncValue`

<details>
<summary>Type declaration</summary>

```tsx
export declare function useAsyncValue(): unknown;
```

</details>

```tsx
function Accessor() {
const data = useAsyncValue();
return <p>{data}</p>;
}

<Await promise={promise}>
<Accessor />
</Await>;
```

This hook returns the resolved data from the nearest `<Await>` component. See the [`<Await>` docs][await docs] for more information.

[await docs]: ../components/await
2 changes: 1 addition & 1 deletion packages/react-router/lib/components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -440,7 +440,7 @@ export function Routes({
}

export interface AwaitResolveRenderFunction {
(data: Awaited<any>): JSX.Element;
(data: Awaited<any>): React.ReactElement;
}

export interface AwaitProps {
Expand Down