Skip to content

[WIP] Rewrite use() docs#8305

Open
rickhanlonii wants to merge 2 commits intoreactjs:mainfrom
rickhanlonii:rewrite-use-docs
Open

[WIP] Rewrite use() docs#8305
rickhanlonii wants to merge 2 commits intoreactjs:mainfrom
rickhanlonii:rewrite-use-docs

Conversation

@rickhanlonii
Copy link
Member

@rickhanlonii rickhanlonii commented Feb 13, 2026

@github-actions
Copy link

Size changes

Details

📦 Next.js Bundle Analysis for react-dev

This analysis was generated by the Next.js Bundle Analysis action. 🤖

This PR introduced no changes to the JavaScript bundle! 🙌

<button onClick={handleRefresh} disabled={isPending}>
{isPending ? 'Refreshing...' : 'Refresh'}
</button>
<ErrorBoundary fallback={<p>⚠️Something went wrong</p>}>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This ErrorBoundary does not seem to be working

Image

async function getAlbums(artistId) {
// Add a fake delay to make waiting noticeable.
await new Promise(resolve => {
setTimeout(resolve, 80);
Copy link
Collaborator

@aurorascharff aurorascharff Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this delay a bit too short? The loading state is very brief in the sandbox. I keep trying to see the loading state in the other two tabs, but the promise resolves too quickly for the difference between hovering/not hovering to be clear.
Trying it in a forked sandbox, 1second seems better.


function handleRefresh() {
startTransition(() => {
setAlbumsPromise(fetchData('/the-beatles/albums'));
Copy link
Collaborator

@aurorascharff aurorascharff Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding some artificial delay in fetchData would make the isPending part of the code and the <Suspense fallback={<p>Loading...</p>}> more useful.

However, without async/await, any potential delay on the refresh will not actually disable the button. I added some delay in a forked sandbox, and had to add async await in front of fetchData for it to correctly disable the refresh button. But I guess that would make the error boundary useless?

As Maxwell said, seems like there's a few issues with this example.

```js
function Albums() {
// 🔴 fetch creates a new Promise on every render.
const albums = use(fetch('/albums'));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding more examples of ways to recreate promises between renders would be helpful, such as some ways you can create a new promise between renders

  1. calling a function that creates a new uncached promise. ie fetch
  2. adding a .then, .catch, .finally on a cached promise
  3. calling an async function
  4. calling functions from the Promise object i.e. Promise.resolve

}
```

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.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This framing reads a bit like we should always prefer use() over awaiting in a Server Component. It might help to clarify that use() doesn’t inherently avoid blocking and that both approaches still depend on Suspense, and the reason we prefer use() is because we need to use a client component.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree! To add to this, the "RSC blocks rendering" framing can read as if it blocks the entire app, when that's not really the case. Maybe we can mention that if we move the async work to a child async Server Component wrapped in <Suspense>, it enables SSR streaming and avoids blocking.

I think this Deep Dive (or somewhere else in these use() docs) would benefit from mentioning the async SC + Suspense pattern and clarifying when each approach is preferable. Also, it's worth noting that neither the Suspense docs nor the RSC docs currently cover this "stremaing SSR" pattern either. Could be a good follow-up to add streaming examples there as well :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing I'm not fully clear on: beyond the bundle size tradeoff, are there other meaningful tradeoffs between the two approaches? Might be worth clarifying that in the Deep Dive as welll

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The big trade-off is when you will have information on the page (i.e., loading states). When a developer uses await for data loading, the rendering of that vDOM/DOM branch is blocked until the promise resolves. If this is set too high on the page, you will see a blank page until all data is ready (as with traditional PHP or Django webpages). When the developer uses theuse hook, the user gets the loading section of the vDOM/DOM branch up to the first suspense boundary, and the rest of the page can render. Giving a much better user experience.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MaxwellCohen You're right in that it blocks rendering until the promise resolves, unless you wrap it in a <Suspense />. You can leverage this in such a way that it only blocks rendering for the UI that strictly depends on the data we're fetching, the rest is immediately sent to the browser.
See this snippet: https://gist.github.com/hernan-yadiel/2b9567df4536120b4c3931846978ada2

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once #8300 is merged, it would be amazing to have a comparison example of both approaches. My rule of thumb is to await for first paint critical path data (Metadata, above the fold data, key product data, etc.) and use use for secondary data (below the fold data, recommendations)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TIL. I definitely believed it was the await itself that was holding up the initial SSR response and that the lack of await (regardless of Suspense usage) is what allows us to get around this. Surprised to find that use() also blocks the initial SSR unless you use Suspense.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This nerd sniped me into creating a quick demo comparison of async and use. The bottom line is that Suspense + use/async is magical when you isolate slow data within Suspense boundaries.

demo link: https://streaming-test-three.vercel.app/comparison
repo: https://github.com/maxwellCohen/streaming-test/


#### Fetching data with `useEffect` {/*fetching-data-with-useeffect*/}

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.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would it make sense to mention this isn't recommended and you should use use. Maybe thats obvious


<Pitfall>

##### Promises passed to `use` must be cached {/*promises-must-cached*/}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really like this section as I think its a common pitfall

// Set status fields so React can read the value
// synchronously if the Promise resolves before
// `use` is called (e.g. when preloading on hover).
promise.status = 'pending';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

interesting hadn't known it was preferred too set the status fields like this

```jsx
function Albums({ albumsPromise }) {
try {
// ❌ Don't wrap `use` in try-catch
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this trip quite a few people up? Its interesting because its pretty obvious when you know use throws internally. Good callout forsure

@brenelz
Copy link

brenelz commented Feb 14, 2026

Overall looks great. Nice work!

* `promise`: A [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) whose resolved value you want to read. The Promise must be [cached](#caching-promises-for-client-components) so that the same instance is reused across re-renders.

* The `use` API must be called inside a Component or a Hook.
* 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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious why this caveat was removed? Is it no longer the preferred way?

I know it could depend of the use case, but I did follow this suggestion as it helps shipping smaller initial bundles, instead of use(dataPromise) where the component where it was called from is still part of the initial bundle.

* `use` must be called inside a Component or a Hook.
* `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.
* 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)
* 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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the 'use client' docs has a section with a list of serializable props, we could link to this page

Suggested change
* 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.
* When passing a Promise from a Server Component to a Client Component, its resolved value must be [serializable](https://react.dev/reference/rsc/use-client#serializable-types) to pass between server and client. Data types like functions aren't serializable and cannot be the resolved value of such a Promise.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[...] to pass between server and client.

nit: for me this phrase feels redundant here, and it reads a bit weird when I first read it. It also subtly shifts from React terminology to infra terminology, which could be confusing since RSC don't necessarily imply a physical server. I would just drop that phrase:

Suggested change
* 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.
* When passing a Promise from a Server Component to a Client Component, its resolved value must be serializable. Data types like functions aren't serializable and cannot be the resolved value of such a Promise.

Copy link
Collaborator

@stephan-noel stephan-noel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just left some questions small nits, looks great overall.

### Reading context with `use` {/*reading-context-with-use*/}

When a [context](/learn/passing-data-deeply-with-context) is passed to `use`, it works similarly to [`useContext`](/reference/react/useContext). While `useContext` must be called at the top level of your component, `use` can be called inside conditionals like `if` and loops like `for`. `use` is preferred over `useContext` because it is more flexible.
When a [context](/learn/passing-data-deeply-with-context) is passed to `use`, it works similarly to [`useContext`](/reference/react/useContext). While `useContext` must be called at the top level of your component, `use` can be called inside conditionals like `if` and loops like `for`.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that there are 2 ways to read context, use and useContext, it seems that there is really no reason to use useContext anymore. Not sure if it's worth clarifying any future plans or lack thereof.

```js [[1, 4, "App"], [2, 2, "Message"], [3, 7, "Suspense"], [4, 8, "messagePromise", 30], [4, 5, "messagePromise"]]
export default function App() {
const [albums, setAlbums] = useState(null);
const [isLoading, setIsLoading] = useState(true);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This requires managing loading and error states manually, and the component renders empty on first paint before the Effect fires.

Emphasis mine. The component doesn't render empty on first paint because the default state is set to true. Just confused me a little bit when I saw it not rendering empty.


#### Caveats {/*promise-caveats*/}

* `use` must be called inside a Component or a Hook.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I know it's implied in the title and by virtue of being in the APIs section instead of the hooks section, but I wonder if it's worth explicitly calling out that use is not a hook despite it starting with the "use" naming convention.

Just by habit and this restriction being similar to one of the the "rules of hooks" might be confusing.


export function fetchData(url) {
if (!cache.has(url)) {
cache.set(url, getData(url));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm trying to reconcile this with the Rules of React. My first thought is that this is "okay" because it falls under lazy initialization. Is that how you think about it?

In the example given on that page, it says "Good: if it doesn't affect other components" and this will affect other components in some sense, but I guess it affects them in a way that doesn't really matter.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way I think about use for promises is that it is like an useEffect, where you pass in a promise dependency. As the promise changes status, use will run various actions (throwing an error if not resolved) and rerender when resolved. Just like you cannot put a newly created function/object in an useEffect depency array, you cannot pass a new promise into use

I am very open to other ways of thinking about this.

}
```

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.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TIL. I definitely believed it was the await itself that was holding up the initial SSR response and that the lack of await (regardless of Suspense usage) is what allows us to get around this. Surprised to find that use() also blocks the initial SSR unless you use Suspense.


<Note>

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.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you wanted to use React without a framework and just use use() either via an underlying library or building your own, would that also be considered valid usage? I think I recall talks of trying to replace useEffect in react-query with use.

It's just framework made me think meta-framework as in only NextJS, RR7, Tanstack Start, etc.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants