Skip to content

Commit 252b67e

Browse files
committed
Enhance documentation on using Promises in Client Components with use
This commit adds important guidelines regarding the caching and stability of Promises passed to the `use` API in Client Components. It includes examples demonstrating how to properly cache Promises to prevent components from re-suspending on every render, as well as addressing pitfalls related to using uncached and chained Promises.
1 parent bd87c39 commit 252b67e

File tree

1 file changed

+167
-0
lines changed
  • src/content/reference/react

1 file changed

+167
-0
lines changed

src/content/reference/react/use.md

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ The `use` API returns the value that was read from the resource like the resolve
5050
* The `use` API must be called inside a Component or a Hook.
5151
* When fetching data in a [Server Component](/reference/rsc/server-components), prefer `async` and `await` over `use`. `async` and `await` pick up rendering from the point where `await` was invoked, whereas `use` re-renders the component after the data is resolved.
5252
* Prefer creating Promises in [Server Components](/reference/rsc/server-components) and passing them to [Client Components](/reference/rsc/use-client) over creating Promises in Client Components. Promises created in Client Components are recreated on every render. Promises passed from a Server Component to a Client Component are stable across re-renders. [See this example](#streaming-data-from-server-to-client).
53+
* A Promise used in Client Components and passed to `use` must be cached or stable between renders (e.g. not recreated between renders); otherwise each render creates a new Promise and the component may suspend indefinitely.
54+
* A Promise passed to `use` that comes from chaining [`.then`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then), [`.catch`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/catch), or [`.finally`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/finally) must also be cached or stable between renders; each call returns a new Promise.
55+
5356
5457
---
5558
@@ -316,6 +319,139 @@ But using `await` in a [Server Component](/reference/rsc/server-components) will
316319
317320
</DeepDive>
318321
322+
### Using Promises in Client Components {/*using-promises-in-client-components*/}
323+
324+
When you pass a Promise to `use` that was created in a Client Component, it must be cached or stable between renders. One way to do that is to memoize the async work by input—for example, a cache keyed by the same arguments returns the same Promise for the same arguments. In this example, `getDoubleCountCached(count)` returns the same Promise for a given `count`, so the component does not re-suspend when re-rendering with the same count.
325+
326+
<Sandpack>
327+
328+
```js src/DoubleCount.js active
329+
import { use } from 'react';
330+
331+
export default function DoubleCount({ count }) {
332+
const doubleCount = use(getDoubleCountCached(count));
333+
334+
return (
335+
<div>
336+
<p>Count: {count}</p>
337+
<p>Double count: {doubleCount}</p>
338+
</div>
339+
);
340+
}
341+
342+
function getDoubleCount(count) {
343+
return new Promise((resolve) =>
344+
setTimeout(() => resolve(count * 2), 500)
345+
);
346+
}
347+
348+
function cacheFn(fn) {
349+
const cacheMap = new Map();
350+
return (...args) => {
351+
const key = JSON.stringify(args);
352+
if (cacheMap.has(key)) {
353+
return cacheMap.get(key);
354+
}
355+
const r = fn(...args);
356+
cacheMap.set(key, r);
357+
return r;
358+
};
359+
}
360+
361+
const getDoubleCountCached = cacheFn(getDoubleCount);
362+
```
363+
364+
```js src/App.js
365+
import { useState, Suspense } from 'react';
366+
import DoubleCount from './DoubleCount.js';
367+
368+
export default function App() {
369+
const [count, setCount] = useState(0);
370+
return (
371+
<div>
372+
<button
373+
onClick={() => {
374+
setCount((c) => c + 1);
375+
}}
376+
>
377+
Increment
378+
</button>
379+
<button
380+
onClick={() => {
381+
setCount((c) => c - 1);
382+
}}
383+
>
384+
Decrement
385+
</button>
386+
<Suspense fallback={<p>🌀 Loading...</p>}>
387+
<DoubleCount count={count} />
388+
</Suspense>
389+
</div>
390+
);
391+
}
392+
```
393+
394+
```js src/index.js hidden
395+
import React, { StrictMode } from 'react';
396+
import { createRoot } from 'react-dom/client';
397+
import './styles.css';
398+
399+
import App from './App';
400+
401+
const root = createRoot(document.getElementById('root'));
402+
root.render(
403+
<StrictMode>
404+
<App />
405+
</StrictMode>
406+
);
407+
```
408+
409+
</Sandpack>
410+
411+
<Pitfall>
412+
413+
##### Using an uncached Promise in a Client Component keeps the app in the loading state. {/*pitfall-uncached-client-promise*/}
414+
415+
If you pass `getDoubleCount(count)` instead of `getDoubleCountCached(count)` to `use`, a new Promise is created on every render. React treats it as a new resource each time, so the component suspends again and the Suspense fallback stays visible. The app will appear stuck on the loading state (or flicker between loading and content). Always cache or otherwise stabilize client-created Promises passed to `use`.
416+
417+
```js
418+
// ❌ New Promise every render — component re-suspends each time
419+
const doubleCount = use(getDoubleCount(count));
420+
421+
// ✅ Same Promise for same count — suspends once per count
422+
const doubleCount = use(getDoubleCountCached(count));
423+
```
424+
425+
##### Chained Promises (`.then`, `.catch`, `.finally`) must be cached too. {/*pitfall-chained-promise*/}
426+
427+
Each call to `.then`, `.catch`, or `.finally` returns a new Promise. If you pass that chained Promise to `use` without caching it, you get a new Promise every render and the component will re-suspend each time.
428+
429+
```js
430+
// ❌ New Promise every render — .then() returns a new Promise each time
431+
const data = use(fetch(url).then((r) => r.json()));
432+
433+
const fetchJsonCached = (() => {
434+
const cache = new Map();
435+
return (url) => {
436+
if (!cache.has(url)) {
437+
cache.set(url, fetch(url).then((r) => r.json()));
438+
}
439+
return cache.get(url);
440+
};
441+
})();
442+
443+
function MyComponent() {
444+
// ✅ Cache the chained Promise so the same reference is used for the same url
445+
const data = use(fetchJsonCached('/api/my-api'));
446+
447+
return <div> {data} </div>
448+
}
449+
```
450+
451+
</Pitfall>
452+
453+
---
454+
319455
### Dealing with rejected Promises {/*dealing-with-rejected-promises*/}
320456
321457
In some cases a Promise passed to `use` could be rejected. You can handle rejected Promises by either:
@@ -438,6 +574,36 @@ To use the Promise's <CodeStep step={1}>`catch`</CodeStep> method, call <CodeSte
438574
439575
## Troubleshooting {/*troubleshooting*/}
440576
577+
### My component stays on the loading state (or keeps flickering) when I use a Promise with `use` {/*promise-not-stable*/}
578+
579+
The Promise you pass to `use` is likely recreated on every render. React treats a new Promise as a new resource, so the component suspends again each time and the Suspense fallback stays visible (or the UI flickers between fallback and content). This often happens when the Promise is created inside a Client Component without being cached or stored, or when you pass the result of `.then()`, `.catch()`, or `.finally()`—each call returns a new Promise, so that chained result must be cached too.
580+
581+
```jsx
582+
// ❌ New Promise every render — component re-suspends each time
583+
function MyComponent({ id }) {
584+
const data = use(fetchData(id)); // fetchData(id) returns a new Promise each render
585+
return <p>{data}</p>;
586+
}
587+
```
588+
589+
Cache or otherwise stabilize the Promise between renders. For example, store it in state, or use a cache keyed by the same inputs so the same Promise is returned for the same arguments. [See the Client Component caching example.](#using-promises-in-client-components)
590+
591+
```jsx
592+
// ✅ Same Promise for same id — cache returns stable reference per argument
593+
const promiseCache = new Map();
594+
function fetchDataCached(id) {
595+
if (!promiseCache.has(id)) promiseCache.set(id, fetchData(id));
596+
return promiseCache.get(id);
597+
}
598+
599+
function MyComponent({ id }) {
600+
const data = use(fetchDataCached(id));
601+
return <p>{data}</p>;
602+
}
603+
```
604+
605+
---
606+
441607
### "Suspense Exception: This is not a real error!" {/*suspense-exception-error*/}
442608
443609
You are either calling `use` outside of a React Component or Hook function, or calling `use` in a try–catch block. If you are calling `use` inside a try–catch block, wrap your component in an Error Boundary, or call the Promise's `catch` to catch the error and resolve the Promise with another value. [See these examples](#dealing-with-rejected-promises).
@@ -460,3 +626,4 @@ function MessageComponent({messagePromise}) {
460626
const message = use(messagePromise);
461627
// ...
462628
```
629+

0 commit comments

Comments
 (0)