Skip to content

Commit ceab6da

Browse files
committed
Address review feedback
1 parent 5fa8c66 commit ceab6da

1 file changed

Lines changed: 101 additions & 35 deletions

File tree

  • src/content/reference/react

src/content/reference/react/use.md

Lines changed: 101 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,10 @@ The resolved value of the Promise.
7474
#### Caveats {/*promise-caveats*/}
7575
7676
* `use` must be called inside a Component or a Hook.
77+
* Despite its name, `use` is not a Hook. Unlike Hooks, it can be called inside loops and conditional statements like `if`.
7778
* `use` cannot be called inside a try-catch block. Instead, wrap your component in an [Error Boundary](#displaying-an-error-with-an-error-boundary) to catch the error and display a fallback.
7879
* Promises passed to `use` must be cached so the same Promise instance is reused across re-renders. [See caching Promises below.](#caching-promises-for-client-components)
79-
* When passing a Promise from a Server Component to a Client Component, its resolved value must be serializable to pass between server and client. Data types like functions aren't serializable and cannot be the resolved value of such a Promise.
80+
* When passing a Promise from a Server Component to a Client Component, its resolved value must be [serializable](/reference/rsc/use-client#serializable-types). Data types like functions aren't serializable and cannot be the resolved value of such a Promise.
8081
8182
---
8283
@@ -252,7 +253,7 @@ The component that calls <CodeStep step={1}>`use`</CodeStep> must be wrapped in
252253
253254
#### Fetching data with `use` {/*fetching-data-with-use*/}
254255
255-
Calling `use` with a cached Promise is the recommended way to fetch data. The component suspends while the Promise is pending, and React displays the nearest Suspense fallback. Rejected Promises propagate to the nearest [Error Boundary](/reference/react/Component#catching-rendering-errors-with-an-error-boundary).
256+
In this example, `Albums` calls `use` with a cached Promise. The component suspends while the Promise is pending, and React displays the nearest Suspense fallback. Rejected Promises propagate to the nearest [Error Boundary](/reference/react/Component#catching-rendering-errors-with-an-error-boundary).
256257
257258
<Sandpack>
258259
@@ -355,7 +356,7 @@ async function getAlbums() {
355356
356357
#### Fetching data with `useEffect` {/*fetching-data-with-useeffect*/}
357358
358-
Without `use`, a common approach is to fetch data in an Effect and update state when the data arrives. This requires managing loading and error states manually, and the component renders empty on first paint before the Effect fires.
359+
Before `use`, a common approach was to fetch data in an Effect and update state when the data arrives. Compared to `use`, this approach requires managing loading and error states manually. For more details on why fetching in an Effect is discouraged, see [You Might Not Need an Effect](/learn/you-might-not-need-an-effect#fetching-data).
359360
360361
<Sandpack>
361362
@@ -439,10 +440,19 @@ export async function fetchAlbums() {
439440
440441
Promises created inside a component are recreated on every render. This causes React to show the Suspense fallback repeatedly and prevents content from appearing. Instead, pass a Promise from a cache, a Suspense-enabled framework, or a Server Component.
441442
443+
Common ways a Promise can be unintentionally recreated on every render:
444+
442445
```js
443446
function Albums() {
444-
// 🔴 fetch creates a new Promise on every render.
447+
// 🔴 `fetch` creates a new Promise on every render.
445448
const albums = use(fetch('/albums'));
449+
450+
// 🔴 Calling an async function creates a new Promise on every render.
451+
const albums = use(getAlbums());
452+
453+
// 🔴 Adding `.then` returns a new Promise on every render,
454+
// even if `fetchData` is cached.
455+
const albums = use(fetchData('/albums').then(res => res.json()));
446456
// ...
447457
}
448458
```
@@ -452,7 +462,7 @@ function Albums() {
452462
const albums = use(fetchData('/albums'));
453463
```
454464
455-
Ideally, Promises are created before renderingsuch as in an event handler, a route loader, or a Server Component and passed to the component that calls `use`. Fetching lazily in render delays network requests and can create waterfalls.
465+
Ideally, Promises are created before rendering, such as in an event handler, a route loader, or a Server Component, and passed to the component that calls `use`. Fetching lazily in render delays network requests and can create waterfalls.
456466
457467
</Pitfall>
458468
@@ -478,10 +488,12 @@ The `fetchData` function returns the same Promise each time it's called with the
478488
479489
<Note>
480490
481-
The way you cache Promises depends on the framework you use with Suspense. Frameworks typically provide built-in caching mechanisms. If you don't use a framework, you can use a simple module-level cache like the one above, or a library that supports Suspense-compatible caching.
491+
The way you cache Promises depends on the framework you use with Suspense. Frameworks typically provide built-in caching mechanisms. If you don't use a framework, you can use a simple module-level cache like the one above, or a [Suspense-enabled data source](/reference/react/Suspense#displaying-a-fallback-while-content-is-loading).
482492
483493
</Note>
484494
495+
In the example below, clicking "Re-render" updates state in `App` and triggers a re-render. Because `fetchData` returns the same cached Promise, `Albums` reads the value synchronously instead of showing the Suspense fallback again.
496+
485497
<Sandpack>
486498
487499
```js src/App.js active
@@ -567,7 +579,7 @@ async function getAlbums() {
567579
568580
#### How to implement a promise cache {/*how-to-implement-a-promise-cache*/}
569581
570-
A basic cache stores the Promise keyed by URL so the same instance is reused across renders. To also avoid unnecessary Suspense fallbacks when data is already available, you can set `status` and `value` (or `reason`) fields on the Promise. React checks these fields when `use` is called if `status` is `'fulfilled'`, it reads `value` synchronously without suspending. If `status` is `'rejected'`, it throws `reason`. If the field is missing or `'pending'`, it suspends.
582+
A basic cache stores the Promise keyed by URL so the same instance is reused across renders. To also avoid unnecessary Suspense fallbacks when data is already available, you can set `status` and `value` (or `reason`) fields on the Promise. React checks these fields when `use` is called: if `status` is `'fulfilled'`, it reads `value` synchronously without suspending. If `status` is `'rejected'`, it throws `reason`. If the field is missing or `'pending'`, it suspends.
571583
572584
```js
573585
let cache = new Map();
@@ -758,7 +770,7 @@ button { margin-bottom: 10px; }
758770
759771
<Note>
760772
761-
Frameworks that support Suspense typically provide their own caching and invalidation mechanisms. Building a custom cache like the one above is useful for understanding the pattern, but in practice you should use your framework's data fetching solution.
773+
Frameworks that support Suspense typically provide their own caching and invalidation mechanisms. The custom cache above is useful for understanding the pattern, but in practice prefer your framework's data fetching solution.
762774
763775
</Note>
764776
@@ -887,7 +899,7 @@ async function getData(url) {
887899
async function getAlbums(artistId) {
888900
// Add a fake delay to make waiting noticeable.
889901
await new Promise(resolve => {
890-
setTimeout(resolve, 80);
902+
setTimeout(resolve, 800);
891903
});
892904

893905
if (artistId === 'the-beatles') {
@@ -1047,16 +1059,33 @@ root.render(
10471059
10481060
#### Should I resolve a Promise in a Server or Client Component? {/*resolve-promise-in-server-or-client-component*/}
10491061
1050-
A Promise can be passed from a Server Component to a Client Component and resolved in the Client Component with the `use` API. You can also resolve the Promise in a Server Component with `await` and pass the required data to the Client Component as a prop.
1062+
A Promise can be passed from a Server Component to a Client Component and resolved in the Client Component with the `use` API. You can also resolve the Promise in a Server Component with `await` and pass the resolved value to the Client Component as a prop. The difference comes down to how much of your UI you want to reveal before the data is ready.
1063+
1064+
Using `await` in a [Server Component](/reference/rsc/server-components) suspends the Server Component itself, so React won't continue rendering it until the Promise resolves:
10511065
10521066
```js
1067+
// Server Component
10531068
export default async function App() {
10541069
const messageContent = await fetchMessage();
1055-
return <Message messageContent={messageContent} />
1070+
return <Message messageContent={messageContent} />;
1071+
}
1072+
```
1073+
1074+
Passing the Promise to a Client Component doesn't suspend the Server Component. The Server Component returns immediately, and the Client Component suspends when it calls `use`:
1075+
1076+
```js
1077+
// Server Component
1078+
export default function App() {
1079+
const messagePromise = fetchMessage();
1080+
return <Message messagePromise={messagePromise} />;
10561081
}
10571082
```
10581083
1059-
But using `await` in a [Server Component](/reference/rsc/server-components) will block its rendering until the `await` statement is finished. Passing a Promise from a Server Component to a Client Component prevents the Promise from blocking the rendering of the Server Component.
1084+
In both cases, the component that reads the Promise suspends. Make sure there's a [`<Suspense>`](/reference/react/Suspense) boundary above it so React can display a fallback there and keep rendering the rest of the page. Without a boundary above the suspending component, the suspension bubbles up the tree and blocks the surrounding UI until the Promise resolves.
1085+
1086+
Passing the Promise down is useful when a Client Component needs the data and you want to reveal as much of the surrounding UI as possible while the Promise is pending. Because the Client Component is the component that suspends, you can place the `<Suspense>` boundary close to it and let the rest of the page render right away. See [Displaying a fallback while content is loading](/reference/react/Suspense#displaying-a-fallback-while-content-is-loading) for more on boundary placement.
1087+
1088+
When the surrounding UI is small or depends on the same data, prefer `await` on the server. If a Server Component above already awaits the data, pass the resolved value down instead of creating a new Promise to call `use`.
10601089
10611090
</DeepDive>
10621091
@@ -1066,36 +1095,40 @@ But using `await` in a [Server Component](/reference/rsc/server-components) will
10661095
10671096
If the Promise passed to `use` is rejected, the error propagates to the nearest [Error Boundary](/reference/react/Component#catching-rendering-errors-with-an-error-boundary). Wrap the component that calls `use` in an Error Boundary to display a fallback when the Promise is rejected.
10681097
1098+
In the example below, `fetchData` rejects on the first attempt and succeeds on retry. The Error Boundary catches the rejection and shows a fallback with a "Try again" button.
1099+
10691100
<Sandpack>
10701101
10711102
```js src/App.js active
1072-
import { use, Suspense, useState, useTransition } from "react";
1103+
import { use, Suspense, useState, startTransition } from "react";
10731104
import { ErrorBoundary } from "react-error-boundary";
1074-
import { fetchData } from "./data.js";
1105+
import { fetchData, refetchData } from "./data.js";
10751106

10761107
export default function App() {
10771108
const [albumsPromise, setAlbumsPromise] = useState(
10781109
() => fetchData('/the-beatles/albums')
10791110
);
1080-
const [isPending, startTransition] = useTransition();
10811111

1082-
function handleRefresh() {
1112+
function handleRetry() {
10831113
startTransition(() => {
1084-
setAlbumsPromise(fetchData('/the-beatles/albums'));
1114+
setAlbumsPromise(refetchData('/the-beatles/albums'));
10851115
});
10861116
}
10871117

10881118
return (
1089-
<>
1090-
<button onClick={handleRefresh} disabled={isPending}>
1091-
{isPending ? 'Refreshing...' : 'Refresh'}
1092-
</button>
1093-
<ErrorBoundary fallback={<p>⚠️Something went wrong</p>}>
1094-
<Suspense fallback={<p>Loading...</p>}>
1095-
<Albums albumsPromise={albumsPromise} />
1096-
</Suspense>
1097-
</ErrorBoundary>
1098-
</>
1119+
<ErrorBoundary
1120+
resetKeys={[albumsPromise]}
1121+
fallbackRender={() => (
1122+
<>
1123+
<p>⚠️ Something went wrong loading the albums.</p>
1124+
<button onClick={handleRetry}>Try again</button>
1125+
</>
1126+
)}
1127+
>
1128+
<Suspense fallback={<p>Loading...</p>}>
1129+
<Albums albumsPromise={albumsPromise} />
1130+
</Suspense>
1131+
</ErrorBoundary>
10991132
);
11001133
}
11011134

@@ -1118,17 +1151,50 @@ function Albums({ albumsPromise }) {
11181151
// the framework that you use together with Suspense.
11191152
// Normally, the caching logic would be inside a framework.
11201153

1121-
async function getData(url) {
1122-
if (url === '/the-beatles/albums') {
1123-
// This fetch will always fail to demonstrate the error boundary.
1124-
throw new Error('Failed to fetch albums');
1125-
} else {
1126-
throw Error('Not implemented');
1154+
let cache = new Map();
1155+
let retried = false;
1156+
1157+
export function fetchData(url) {
1158+
if (!cache.has(url)) {
1159+
cache.set(url, getData(url));
11271160
}
1161+
return cache.get(url);
11281162
}
11291163

1130-
export function fetchData(url) {
1131-
return getData(url);
1164+
export function refetchData(url) {
1165+
cache.delete(url);
1166+
retried = true;
1167+
return fetchData(url);
1168+
}
1169+
1170+
async function getData(url) {
1171+
// Add a fake delay to make the loading state visible.
1172+
await new Promise(resolve => setTimeout(resolve, 1000));
1173+
if (url === '/the-beatles/albums') {
1174+
// Fail the first attempt to demonstrate the Error Boundary,
1175+
// then succeed on retry.
1176+
if (!retried) {
1177+
throw new Error('Example Error: Failed to fetch albums');
1178+
}
1179+
return [{
1180+
id: 13,
1181+
title: 'Let It Be',
1182+
year: 1970
1183+
}, {
1184+
id: 12,
1185+
title: 'Abbey Road',
1186+
year: 1969
1187+
}, {
1188+
id: 11,
1189+
title: 'Yellow Submarine',
1190+
year: 1969
1191+
}, {
1192+
id: 10,
1193+
title: 'The Beatles',
1194+
year: 1968
1195+
}];
1196+
}
1197+
throw new Error('Not implemented');
11321198
}
11331199
```
11341200

0 commit comments

Comments
 (0)