From 8bb31acb86bf68fa33d97dd0f1b834dfa71e2b1a Mon Sep 17 00:00:00 2001
From: Jack Pope
Date: Wed, 17 Jun 2026 10:38:02 -0700
Subject: [PATCH 1/9] Expand FragmentInstance reference docs (#8309)
* Clean up and expand fragment ref docs
* Add cached intersection observer usage example
* Expand focus example to show search
* Small clean ups
* Add FragmentInstance operations reference list
* Add reference to scrollIntoView operation
* Migrate examples to sandpack
---
src/content/reference/react/Fragment.md | 824 ++++++++++++++++++++++--
1 file changed, 763 insertions(+), 61 deletions(-)
diff --git a/src/content/reference/react/Fragment.md b/src/content/reference/react/Fragment.md
index 5ffc45277..14a534bbc 100644
--- a/src/content/reference/react/Fragment.md
+++ b/src/content/reference/react/Fragment.md
@@ -6,7 +6,7 @@ title: (<>...>)
``, often used via `<>...>` syntax, lets you group elements without a wrapper node.
- Fragments can also accept refs, which enable interacting with underlying DOM nodes without adding wrapper elements. See reference and usage below.
+Fragments can also accept refs, which enable interacting with underlying DOM nodes without adding wrapper elements.
```js
<>
@@ -30,41 +30,258 @@ Wrap elements in `` to group them together in situations where you nee
#### Props {/*props*/}
- **optional** `key`: Fragments declared with the explicit `` syntax may have [keys.](/learn/rendering-lists#keeping-list-items-in-order-with-key)
-- **optional** `ref`: A ref object (e.g. from [`useRef`](/reference/react/useRef)) or [callback function](/reference/react-dom/components/common#ref-callback). React provides a `FragmentInstance` as the ref value that implements methods for interacting with the DOM nodes wrapped by the Fragment.
+- **optional** `ref`: A ref object (e.g. from [`useRef`](/reference/react/useRef)) or [callback function](/reference/react-dom/components/common#ref-callback). React provides a `FragmentInstance` as the ref value that implements methods for interacting with the DOM nodes wrapped by the Fragment.
-### FragmentInstance {/*fragmentinstance*/}
+#### Caveats {/*caveats*/}
-When you pass a ref to a fragment, React provides a `FragmentInstance` object with methods for interacting with the DOM nodes wrapped by the fragment:
+* If you want to pass `key` to a Fragment, you can't use the `<>...>` syntax. You have to explicitly import `Fragment` from `'react'` and render `...`.
-**Event handling methods:**
-- `addEventListener(type, listener, options?)`: Adds an event listener to all first-level DOM children of the Fragment.
-- `removeEventListener(type, listener, options?)`: Removes an event listener from all first-level DOM children of the Fragment.
-- `dispatchEvent(event)`: Dispatches an event to a virtual child of the Fragment to call any added listeners and can bubble to the DOM parent.
+* React does not [reset state](/learn/preserving-and-resetting-state) when you go from rendering `<>>` to `[]` or back, or when you go from rendering `<>>` to `` and back. This only works a single level deep: for example, going from `<><>>>` to `` resets the state. See the precise semantics [here.](https://gist.github.com/clemmy/b3ef00f9507909429d8aa0d3ee4f986b)
-**Layout methods:**
-- `compareDocumentPosition(otherNode)`: Compares the document position of the Fragment with another node.
- - If the Fragment has children, the native `compareDocumentPosition` value is returned.
- - Empty Fragments will attempt to compare positioning within the React tree and include `Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC`.
- - Elements that have a different relationship in the React tree and DOM tree due to portaling or other insertions are `Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC`.
-- `getClientRects()`: Returns a flat array of `DOMRect` objects representing the bounding rectangles of all children.
-- `getRootNode()`: Returns the root node containing the Fragment's parent DOM node.
+* If you want to pass `ref` to a Fragment, you can't use the `<>...>` syntax. You have to explicitly import `Fragment` from `'react'` and render `...`.
-**Focus management methods:**
-- `focus(options?)`: Focuses the first focusable DOM node in the Fragment. Focus is attempted on nested children depth-first.
-- `focusLast(options?)`: Focuses the last focusable DOM node in the Fragment. Focus is attempted on nested children depth-first.
-- `blur()`: Removes focus if `document.activeElement` is within the Fragment.
+---
-**Observer methods:**
-- `observeUsing(observer)`: Starts observing the Fragment's DOM children with an IntersectionObserver or ResizeObserver.
-- `unobserveUsing(observer)`: Stops observing the Fragment's DOM children with the specified observer.
+### `FragmentInstance` {/*fragmentinstance*/}
-#### Caveats {/*caveats*/}
+When you pass a `ref` to a Fragment, React provides a `FragmentInstance` object. It implements methods for interacting with the first-level DOM children wrapped by the Fragment.
+
+* [`addEventListener`](#addeventlistener) and [`removeEventListener`](#removeeventlistener) manage event listeners across all first-level DOM children.
+* [`dispatchEvent`](#dispatchevent) dispatches an event on the Fragment, which can bubble to the DOM parent.
+* [`focus`](#focus), [`focusLast`](#focuslast), and [`blur`](#blur) manage focus across all nested children depth-first.
+* [`observeUsing`](#observeusing) and [`unobserveUsing`](#unobserveusing) attach and detach `IntersectionObserver` or `ResizeObserver` instances.
+* [`getClientRects`](#getclientrects) returns bounding rectangles of all first-level DOM children.
+* [`getRootNode`](#getrootnode) returns the root node of the Fragment's parent.
+* [`compareDocumentPosition`](#comparedocumentposition) compares the Fragment's position with another node.
+* [`scrollIntoView`](#scrollintoview) scrolls the Fragment's children into view.
+
+---
+
+#### `addEventListener(type, listener, options?)` {/*addeventlistener*/}
+
+Adds an event listener to all first-level DOM children of the Fragment.
+
+```js
+fragmentRef.current.addEventListener('click', handleClick);
+```
+
+##### Parameters {/*addeventlistener-parameters*/}
+
+* `type`: A string representing the event type to listen for (e.g. `'click'`, `'focus'`).
+* `listener`: The event handler function.
+* **optional** `options`: An options object or boolean for capture, matching the [DOM `addEventListener` API.](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener)
+
+##### Returns {/*addeventlistener-returns*/}
+
+`addEventListener` does not return anything (`undefined`).
+
+---
+
+#### `removeEventListener(type, listener, options?)` {/*removeeventlistener*/}
+
+Removes an event listener from all first-level DOM children of the Fragment.
+
+```js
+fragmentRef.current.removeEventListener('click', handleClick);
+```
+
+##### Parameters {/*removeeventlistener-parameters*/}
+
+* `type`: The event type string.
+* `listener`: The event handler function to remove.
+* **optional** `options`: An options object or boolean, matching the [DOM `removeEventListener` API.](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener)
+
+##### Returns {/*removeeventlistener-returns*/}
+
+`removeEventListener` does not return anything (`undefined`).
+
+---
+
+#### `dispatchEvent(event)` {/*dispatchevent*/}
+
+Dispatches an event on the Fragment. Added event listeners are called, and the event can bubble to the Fragment's DOM parent.
+
+```js
+fragmentRef.current.dispatchEvent(new Event('custom', { bubbles: true }));
+```
+
+##### Parameters {/*dispatchevent-parameters*/}
+
+* `event`: An [`Event`](https://developer.mozilla.org/en-US/docs/Web/API/Event) object to dispatch. If `bubbles` is `true`, the event bubbles to the Fragment's parent DOM node.
+
+##### Returns {/*dispatchevent-returns*/}
+
+`true` if the event was not cancelled, `false` if `preventDefault()` was called.
+
+---
+
+#### `focus(options?)` {/*focus*/}
+
+Focuses the first focusable DOM node in the Fragment. Unlike calling `element.focus()` on a DOM element, this method searches *all* nested children depth-first until it finds a focusable element—not just the element itself or its direct children.
+
+```js
+fragmentRef.current.focus();
+```
+
+##### Parameters {/*focus-parameters*/}
+
+* **optional** `options`: A [`FocusOptions`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) object (e.g. `{ preventScroll: true }`).
+
+##### Returns {/*focus-returns*/}
+
+`focus` does not return anything (`undefined`).
+
+---
+
+#### `focusLast(options?)` {/*focuslast*/}
+
+Focuses the last focusable DOM node in the Fragment. Searches nested children depth-first, then iterates in reverse.
+
+```js
+fragmentRef.current.focusLast();
+```
+
+##### Parameters {/*focuslast-parameters*/}
+
+* **optional** `options`: A [`FocusOptions`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) object.
+
+##### Returns {/*focuslast-returns*/}
+
+`focusLast` does not return anything (`undefined`).
+
+---
+
+#### `blur()` {/*blur*/}
+
+Removes focus from the active element if it is within the Fragment. If `document.activeElement` is not within the Fragment, `blur` does nothing.
+
+```js
+fragmentRef.current.blur();
+```
+
+##### Returns {/*blur-returns*/}
+
+`blur` does not return anything (`undefined`).
+
+---
+
+#### `observeUsing(observer)` {/*observeusing*/}
+
+Starts observing all first-level DOM children of the Fragment with the provided observer.
+
+```js
+const observer = new IntersectionObserver(callback, options);
+fragmentRef.current.observeUsing(observer);
+```
+
+##### Parameters {/*observeusing-parameters*/}
-- If you want to pass `key` to a Fragment, you can't use the `<>...>` syntax. You have to explicitly import `Fragment` from `'react'` and render `...`.
+* `observer`: An [`IntersectionObserver`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver) or [`ResizeObserver`](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver) instance.
-- React does not [reset state](/learn/preserving-and-resetting-state) when you go from rendering `<>>` to `[]` or back, or when you go from rendering `<>>` to `` and back. This only works a single level deep: for example, going from `<><>>>` to `` resets the state. See the precise semantics [here.](https://gist.github.com/clemmy/b3ef00f9507909429d8aa0d3ee4f986b)
+##### Returns {/*observeusing-returns*/}
-- If you want to pass `ref` to a Fragment, you can't use the `<>...>` syntax. You have to explicitly import `Fragment` from `'react'` and render `...`.
+`observeUsing` does not return anything (`undefined`).
+
+---
+
+#### `unobserveUsing(observer)` {/*unobserveusing*/}
+
+Stops observing the Fragment's DOM children with the specified observer.
+
+```js
+fragmentRef.current.unobserveUsing(observer);
+```
+
+##### Parameters {/*unobserveusing-parameters*/}
+
+* `observer`: The same `IntersectionObserver` or `ResizeObserver` instance previously passed to [`observeUsing`](#observeusing).
+
+##### Returns {/*unobserveusing-returns*/}
+
+`unobserveUsing` does not return anything (`undefined`).
+
+---
+
+#### `getClientRects()` {/*getclientrects*/}
+
+Returns a flat array of [`DOMRect`](https://developer.mozilla.org/en-US/docs/Web/API/DOMRect) objects representing the bounding rectangles of all first-level DOM children.
+
+```js
+const rects = fragmentRef.current.getClientRects();
+```
+
+##### Returns {/*getclientrects-returns*/}
+
+An `Array` containing the bounding rectangles of all children.
+
+---
+
+#### `getRootNode(options?)` {/*getrootnode*/}
+
+Returns the root node containing the Fragment's parent DOM node, matching the behavior of [`Node.getRootNode()`](https://developer.mozilla.org/en-US/docs/Web/API/Node/getRootNode).
+
+```js
+const root = fragmentRef.current.getRootNode();
+```
+
+##### Parameters {/*getrootnode-parameters*/}
+
+* **optional** `options`: An object with a `composed` boolean property, matching the [DOM `getRootNode` API.](https://developer.mozilla.org/en-US/docs/Web/API/Node/getRootNode#options)
+
+##### Returns {/*getrootnode-returns*/}
+
+A `Document`, `ShadowRoot`, or the `FragmentInstance` itself if there is no parent DOM node.
+
+---
+
+#### `compareDocumentPosition(otherNode)` {/*comparedocumentposition*/}
+
+Compares the document position of the Fragment with another node, returning a bitmask matching the behavior of [`Node.compareDocumentPosition()`](https://developer.mozilla.org/en-US/docs/Web/API/Node/compareDocumentPosition).
+
+```js
+const position = fragmentRef.current.compareDocumentPosition(otherElement);
+```
+
+##### Parameters {/*comparedocumentposition-parameters*/}
+
+* `otherNode`: The DOM node to compare against.
+
+##### Returns {/*comparedocumentposition-returns*/}
+
+A bitmask of [position flags](https://developer.mozilla.org/en-US/docs/Web/API/Node/compareDocumentPosition#return_value). Empty Fragments and Fragments with children rendered through a [portal](/reference/react-dom/createPortal) include `Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC` in the result.
+
+---
+
+#### `scrollIntoView(alignToTop?)` {/*scrollintoview*/}
+
+Scrolls the Fragment's children into view. When `alignToTop` is `true` or omitted, scrolls to align the first child with the top of the scrollable ancestor. When `alignToTop` is `false`, scrolls to align the last child with the bottom.
+
+```js
+fragmentRef.current.scrollIntoView();
+```
+
+##### Parameters {/*scrollintoview-parameters*/}
+
+* **optional** `alignToTop`: A boolean. If `true` (the default), scrolls the first child to the top of the scrollable area. If `false`, scrolls the last child to the bottom. Unlike [`Element.scrollIntoView()`](https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView), this method does not accept a `ScrollIntoViewOptions` object.
+
+##### Returns {/*scrollintoview-returns*/}
+
+`scrollIntoView` does not return anything (`undefined`).
+
+##### Caveats {/*scrollintoview-caveats*/}
+
+* `scrollIntoView` does not accept an options object. Passing one throws an error. Use the `alignToTop` boolean instead.
+* When the Fragment has no children, `scrollIntoView` scrolls the nearest sibling or parent into view as a fallback.
+
+---
+
+#### `FragmentInstance` Caveats {/*fragmentinstance-caveats*/}
+
+* Methods that target children (such as `addEventListener`, `observeUsing`, and `getClientRects`) operate on *first-level host (DOM) children* of the Fragment. They do not directly target children nested inside another DOM element.
+* `focus` and `focusLast` search nested children depth-first for focusable elements, unlike event and observer methods which only target first-level host children.
+* `observeUsing` does not work on text nodes. React logs a warning in development if the Fragment contains only text children.
+* React does not apply event listeners added via `addEventListener` to hidden [``](/reference/react/Activity) trees. When an `Activity` boundary switches from hidden to visible, listeners are applied automatically.
+* Each first-level DOM child of a Fragment with a `ref` gets a `reactFragments` property—a `Set` containing all Fragment instances that own the element. This enables [caching a shared observer](#caching-global-intersection-observer) across multiple Fragments.
---
@@ -242,47 +459,312 @@ function PostBody({ body }) {
---
-### Using Fragment refs for DOM interaction {/*using-fragment-refs-for-dom-interaction*/}
+### Adding event listeners without a wrapper element {/*adding-event-listeners-without-wrapper*/}
-Fragment refs allow you to interact with the DOM nodes wrapped by a Fragment without adding extra wrapper elements. This is useful for event handling, visibility tracking, focus management, and replacing deprecated patterns like `ReactDOM.findDOMNode()`.
+Fragment `ref`s let you add event listeners to a group of elements without adding a wrapper DOM node. Use a [ref callback](/reference/react-dom/components/common#ref-callback) to attach and clean up listeners:
+
+
```js
-import { Fragment } from 'react';
+import { Fragment, useState, useRef, useEffect } from 'react';
function ClickableFragment({ children, onClick }) {
+ const fragmentRef = useRef(null);
+ useEffect(() => {
+ const fragmentInstance = fragmentRef.current;
+ if (fragmentInstance === null) {
+ return;
+ }
+ fragmentInstance.addEventListener('click', onClick);
+ return () => {
+ fragmentInstance.removeEventListener(
+ 'click',
+ onClick
+ );
+ };
+ }, [onClick])
return (
- {
- fragmentInstance.addEventListener('click', handleClick);
- return () => fragmentInstance.removeEventListener('click', handleClick);
- }}>
+
{children}
);
}
+
+export default function App() {
+ const [clicks, setClicks] = useState(0);
+
+ return (
+ <>
+
Total clicks: {clicks}
+ {
+ setClicks(c => c + 1);
+ }}>
+
+
+
+
+ >
+ );
+}
```
+
+```json package.json hidden
+{
+ "dependencies": {
+ "react": "canary",
+ "react-dom": "canary",
+ "react-scripts": "latest"
+ }
+}
+```
+
+
+
+The `addEventListener` call applies the listener to every first-level DOM child of the Fragment. When children are dynamically added or removed, the `FragmentInstance` automatically adds or removes the listener.
+
+
+
+#### Which children does a Fragment ref target? {/*which-children-does-a-fragment-ref-target*/}
+
+A `FragmentInstance` targets the **first-level host (DOM) children** of the Fragment. Consider this tree:
+
+```js
+
+
+
+
+
+
+
+
+
+```
+
+`Wrapper` is a React component, so the `FragmentInstance` looks through it to find DOM nodes. The targeted children are `A`, `B`, and `D`. `C` is not targeted because it is nested inside the DOM element `B`.
+
+Methods like `addEventListener`, `observeUsing`, and `getClientRects` operate on these first-level DOM children. `focus` and `focusLast` are different—they search *all* nested children depth-first to find focusable elements.
+
+
+
+---
+
+### Managing focus across a group of elements {/*managing-focus-across-elements*/}
+
+Fragment `ref`s provide `focus`, `focusLast`, and `blur` methods that operate across all DOM nodes within the Fragment:
+
+
+
+```js
+import { Fragment, useRef } from 'react';
+
+function FormFields({ children }) {
+ const fragmentRef = useRef(null);
+
+ return (
+ <>
+
+
+
+
+
+
+ {children}
+
+ >
+ );
+}
+
+// Even though the inputs are deeply nested,
+// focus() searches depth-first to find them.
+export default function App() {
+ return (
+
+
+
+ );
+}
+```
+
+```css
+.buttons {
+ display: flex;
+ gap: 8px;
+ margin-bottom: 10px;
+}
+
+label {
+ display: inline-block;
+}
+```
+
+```json package.json hidden
+{
+ "dependencies": {
+ "react": "canary",
+ "react-dom": "canary",
+ "react-scripts": "latest"
+ }
+}
+```
+
+
+
+Calling `focus()` focuses the `street` input—even though it is nested inside a `
;
+}
+```
+
+```js src/data.js hidden
+// Note: the way you would do data fetching depends on
+// the framework that you use together with Suspense.
+// Normally, the caching logic would be inside a framework.
+
+let cache = new Map();
+
+export function fetchData(url) {
+ if (!cache.has(url)) {
+ cache.set(url, getData(url));
+ }
+ return cache.get(url);
+}
+
+async function getData(url) {
+ if (url === '/albums') {
+ return await getAlbums();
+ } else {
+ throw Error('Not implemented');
+ }
+}
+
+async function getAlbums() {
+ // Add a fake delay to make waiting noticeable.
+ await new Promise(resolve => {
+ setTimeout(resolve, 1000);
+ });
+
+ return [{
+ id: 13,
+ title: 'Let It Be',
+ year: 1970
+ }, {
+ id: 12,
+ title: 'Abbey Road',
+ year: 1969
+ }, {
+ id: 11,
+ title: 'Yellow Submarine',
+ year: 1969
+ }, {
+ id: 10,
+ title: 'The Beatles',
+ year: 1968
+ }];
+}
+```
+
+```json package.json hidden
+{
+ "dependencies": {
+ "react": "19.0.0",
+ "react-dom": "19.0.0",
+ "react-scripts": "^5.0.0",
+ "react-error-boundary": "4.0.3"
+ },
+ "main": "/index.js"
+}
+```
+
+
+
+
+
+#### Fetching data with `useEffect` {/*fetching-data-with-useeffect*/}
+
+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).
+
+
+
+```js src/App.js active
+import { useState, useEffect } from 'react';
+import { fetchAlbums } from './data.js';
+
+export default function App() {
+ const [albums, setAlbums] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ fetchAlbums()
+ .then(data => {
+ setAlbums(data);
+ setIsLoading(false);
+ })
+ .catch(err => {
+ setError(err);
+ setIsLoading(false);
+ });
+ }, []);
+
+ if (isLoading) {
+ return
Loading...
;
+ }
+
+ if (error) {
+ return
Error: {error.message}
;
+ }
+
+ return (
+
+ {albums.map(album => (
+
+ {album.title} ({album.year})
+
+ ))}
+
+ );
+}
+```
+
+```js src/data.js hidden
+export async function fetchAlbums() {
+ // Add a fake delay to make waiting noticeable.
+ await new Promise(resolve => {
+ setTimeout(resolve, 1000);
+ });
+
+ return [{
+ id: 13,
+ title: 'Let It Be',
+ year: 1970
+ }, {
+ id: 12,
+ title: 'Abbey Road',
+ year: 1969
+ }, {
+ id: 11,
+ title: 'Yellow Submarine',
+ year: 1969
+ }, {
+ id: 10,
+ title: 'The Beatles',
+ year: 1968
+ }];
+}
+```
+
+
+
+
+
+
+
+
+
+##### Promises passed to `use` must be cached {/*promises-must-cached*/}
+
+Promises created during render are recreated on every render, which causes React to show the Suspense fallback repeatedly and prevents content from appearing.
+
+```js
+function Albums() {
+ // 🔴 `fetch` creates a new Promise on every render.
+ const albums = use(fetch('/albums'));
+ // ...
+}
+```
+
+Instead, pass a Promise from a cache, a Suspense-enabled framework, or a Server Component:
+
+```js
+// ✅ fetchData reads the Promise from a cache.
+const albums = use(fetchData('/albums'));
+```
+
+
+
+
+
+#### Why are Promises recreated on every render? {/*why-promises-recreated*/}
+
+[React doesn't preserve state for renders that suspended before mounting](/reference/react/Suspense#caveats). After each suspension, React retries rendering from scratch, so any Promise created during render is recreated.
+
+Common ways a Promise can be unintentionally recreated during render:
+
+```js
+function Albums() {
+ // 🔴 `fetch` creates a new Promise on every render.
+ const albums = use(fetch('/albums'));
+
+ // 🔴 Uncached `async` function calls create a new Promise on every render.
+ const albums = use((async () => {
+ const res = await fetch('/albums');
+ return res.json();
+ })());
+
+ // 🔴 Adding `.then` returns a new Promise on every render,
+ // even if `fetchData` is cached.
+ const albums = use(fetchData('/albums').then(res => res.json()));
+ // ...
+}
+```
+
+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.
+
+```js
+// ✅ fetchData reads the Promise from a cache.
+const albums = use(fetchData('/albums'));
+```
+
+
+
+---
+
+### Caching Promises for Client Components {/*caching-promises-for-client-components*/}
+
+Promises passed to `use` in Client Components must be cached so the same Promise instance is reused across re-renders. If a new Promise is created directly in render, React will display the Suspense fallback on every re-render.
+
+```js
+// ✅ Cache the Promise so the same one is reused across renders
+let cache = new Map();
+
+export function fetchData(url) {
+ if (!cache.has(url)) {
+ cache.set(url, getData(url));
+ }
+ return cache.get(url);
+}
+```
+
+The `fetchData` function returns the same Promise each time it's called with the same URL. When `use` receives the same Promise on a re-render, it reads the already-resolved value synchronously without suspending.
+
+
+
+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).
+
+
+
+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.
+
+
+
+```js src/App.js active
+import { use, Suspense, useState } from 'react';
+import { fetchData } from './data.js';
+
+export default function App() {
+ const [count, setCount] = useState(0);
+ return (
+ <>
+
+
+ );
+}
+```
+
+```js src/data.js hidden
+// Note: the way you would do data fetching depends on
+// the framework that you use together with Suspense.
+// Normally, the caching logic would be inside a framework.
+
+let cache = new Map();
+
+export function fetchData(url) {
+ if (!cache.has(url)) {
+ cache.set(url, getData(url));
+ }
+ return cache.get(url);
+}
+
+async function getData(url) {
+ if (url === '/albums') {
+ return await getAlbums();
+ } else {
+ throw Error('Not implemented');
+ }
+}
+
+async function getAlbums() {
+ // Add a fake delay to make waiting noticeable.
+ await new Promise(resolve => {
+ setTimeout(resolve, 1000);
+ });
+
+ return [{
+ id: 13,
+ title: 'Let It Be',
+ year: 1970
+ }, {
+ id: 12,
+ title: 'Abbey Road',
+ year: 1969
+ }, {
+ id: 11,
+ title: 'Yellow Submarine',
+ year: 1969
+ }];
+}
+```
+
+
+
+
+
+#### How to implement a promise cache {/*how-to-implement-a-promise-cache*/}
+
+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.
+
+```js
+let cache = new Map();
+
+function fetchData(url) {
+ if (!cache.has(url)) {
+ const promise = getData(url);
+ promise.status = 'pending';
+ promise.then(
+ value => {
+ promise.status = 'fulfilled';
+ promise.value = value;
+ },
+ reason => {
+ promise.status = 'rejected';
+ promise.reason = reason;
+ },
+ );
+ cache.set(url, promise);
+ }
+ return cache.get(url);
+}
+```
+
+This is primarily useful for library authors building Suspense-compatible data layers. React will set the `status` field itself on Promises that don't have it, but setting it yourself avoids an extra render when the data is already available.
+
+This cache pattern is the foundation for [re-fetching data](#re-fetching-data-in-client-components) (where changing the cache key triggers a new fetch) and [preloading data on hover](#preloading-data-on-hover) (where calling `fetchData` early means the Promise may already be resolved by the time `use` reads it).
+
+
+
+
+
+Don't conditionally call `use` based on whether a Promise is settled. Always call `use` unconditionally and let React handle reading the `status` field. This ensures React DevTools can show that the component may suspend on data.
+
+```js
+// 🔴 Don't conditionally skip `use`
+if (promise.status === 'fulfilled') {
+ return promise.value;
+}
+const value = use(promise);
+```
+
+```js
+// ✅ Always call `use` unconditionally
+const value = use(promise);
+```
-```js [[1, 4, "App"], [2, 2, "Message"], [3, 7, "Suspense"], [4, 8, "messagePromise", 30], [4, 5, "messagePromise"]]
+
+
+---
+
+### Re-fetching data in Client Components {/*re-fetching-data-in-client-components*/}
+
+To refresh data at the same URL (for example, with a "Refresh" button), invalidate the cache entry and start a new fetch inside a [`startTransition`](/reference/react/startTransition). Store the resulting Promise in state to trigger a re-render. While the new Promise is pending, React keeps showing the existing content because the update is inside a Transition.
+
+```js
+function App() {
+ const [albumsPromise, setAlbumsPromise] = useState(fetchData('/albums'));
+ const [isPending, startTransition] = useTransition();
+
+ function handleRefresh() {
+ startTransition(() => {
+ setAlbumsPromise(refetchData('/albums'));
+ });
+ }
+ // ...
+}
+```
+
+`refetchData` clears the old cache entry and starts a new fetch at the same URL. Storing the resulting Promise in state triggers a re-render inside the Transition. On re-render, `Albums` receives the new Promise and `use` suspends on it while React keeps showing the old content.
+
+
+
+```js src/App.js active
+import { Suspense, useState, useTransition } from 'react';
+import { use } from 'react';
+import { fetchData, refetchData } from './data.js';
+
+export default function App() {
+ const [albumsPromise, setAlbumsPromise] = useState(
+ () => fetchData('/the-beatles/albums')
+ );
+ const [isPending, startTransition] = useTransition();
+
+ function handleRefresh() {
+ startTransition(() => {
+ setAlbumsPromise(refetchData('/the-beatles/albums'));
+ });
+ }
+
+ return (
+ <>
+
+
;
+}
+```
+
+```js src/data.js hidden
+// Note: the way you would do data fetching depends on
+// the framework that you use together with Suspense.
+// Normally, the caching logic would be inside a framework.
+
+let cache = new Map();
+
+export function fetchData(url) {
+ if (!cache.has(url)) {
+ cache.set(url, getData(url));
+ }
+ return cache.get(url);
+}
+
+export function refetchData(url) {
+ cache.delete(url);
+ return fetchData(url);
+}
+
+async function getData(url) {
+ if (url.startsWith('/the-beatles/albums')) {
+ return await getAlbums();
+ } else {
+ throw Error('Not implemented');
+ }
+}
+
+async function getAlbums() {
+ // Add a fake delay to make waiting noticeable.
+ await new Promise(resolve => {
+ setTimeout(resolve, 1000);
+ });
+
+ return [{
+ id: 13,
+ title: 'Let It Be',
+ year: 1970
+ }, {
+ id: 12,
+ title: 'Abbey Road',
+ year: 1969
+ }, {
+ id: 11,
+ title: 'Yellow Submarine',
+ year: 1969
+ }, {
+ id: 10,
+ title: 'The Beatles',
+ year: 1968
+ }, {
+ id: 9,
+ title: 'Magical Mystery Tour',
+ year: 1967
+ }];
+}
+```
+
+```css
+button { margin-bottom: 10px; }
+```
+
+
+
+
+
+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.
+
+
+
+---
+
+### Preloading data on hover {/*preloading-data-on-hover*/}
+
+You can start loading data before it's needed by calling `fetchData` during a hover event. Since `fetchData` caches the Promise, the data may already be available by the time the user clicks. If the Promise has resolved by the time `use` reads it, React renders the component immediately without showing a Suspense fallback.
+
+```js
+