diff --git a/beta/src/content/apis/react/Suspense.md b/beta/src/content/apis/react/Suspense.md index e640064c..6425cb80 100644 --- a/beta/src/content/apis/react/Suspense.md +++ b/beta/src/content/apis/react/Suspense.md @@ -2496,6 +2496,31 @@ However, now imagine you're navigating between two different user profiles. In t --- +### Providing a fallback for server errors and server-only content {/*providing-a-fallback-for-server-errors-and-server-only-content*/} + +If you use one of the [streaming server rendering APIs](/apis/react-dom/server) (or a framework that relies on them), React will also use your `` boundaries to handle errors on the server. If a component throws an error on the server, React will not abort the server render. Instead, it will find the closest `` component above it and include its fallback (such as a spinner) into the generated server HTML. The user will see a spinner instead of an error. + +On the client, React will attempt to render the same component again. If it errors on the client too, React will throw the error and display the closest [error boundary.](/apis/react/Component#static-getderivedstatefromerror) However, if it does not error on the client, React will not display the error to the user since the content was eventually displayed successfully. + +You can use this to opt out some components from rendering on the server. To do this, throw an error from them in the server environment and then wrap them in a `` boundary to replace their HTML with fallbacks: + +```js +}> + + + +function Chat() { + if (typeof window === 'undefined') { + throw Error('Chat should only render on the client.'); + } + // ... +} +``` + +The server HTML will include the loading indicator. It will be replaced by the `Chat` component on the client. + +--- + ## Reference {/*reference*/} ### `Suspense` {/*suspense*/} diff --git a/beta/src/content/apis/react/useSyncExternalStore.md b/beta/src/content/apis/react/useSyncExternalStore.md index 59627019..95afca93 100644 --- a/beta/src/content/apis/react/useSyncExternalStore.md +++ b/beta/src/content/apis/react/useSyncExternalStore.md @@ -2,21 +2,425 @@ title: useSyncExternalStore --- - + -This section is incomplete, please see the old docs for [useSyncExternalStore.](https://reactjs.org/docs/hooks-reference.html#usesyncexternalstore) +`useSyncExternalStore` is a React Hook that lets you subscribe to an external store. - +```js +const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?) +``` + - + + +--- + +## Usage {/*usage*/} + +### Subscribing to an external store {/*subscribing-to-an-external-store*/} + +Most of your React components will only read data from their [props,](/learn/passing-props-to-a-component) [state,](/apis/react/useState) and [context.](/apis/react/useContext) However, sometimes a component needs to read some data from some store outside of React that changes over time. This includes: + +* Third-party state management libraries that hold state outside of React. +* Browser APIs that expose a mutable value and events to subscribe to its changes. + +Call `useSyncExternalStore` at the top level of your component to read a value from an external data store. + +```js [[1, 5, "todosStore.subscribe"], [2, 5, "todosStore.getSnapshot"], [3, 5, "todos", 0]] +import { useSyncExternalStore } from 'react'; +import { todosStore } from './todoStore.js'; + +function TodosApp() { + const todos = useSyncExternalStore(todosStore.subscribe, todosStore.getSnapshot); + // ... +} +``` + +It returns the snapshot of the data in the store. You need to pass two functions as arguments: + +1. The `subscribe` function should subscribe to the store and return a function that unsubscribes. +2. The `getSnapshot` function should read a snapshot of the data from the store. + +React will use these functions to keep your component subscribed to the store and re-render it on changes. -`useSyncExternalStore` is a hook recommended for reading and subscribing from external data sources in a way that’s compatible with concurrent rendering features like selective hydration and time slicing. +For example, in the sandbox below, `todosStore` is implemented as an external store that stores data outside of React. The `TodosApp` component connects to that external store with the `useSyncExternalStore` Hook. + + ```js -const state = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?); +import { useSyncExternalStore } from 'react'; +import { todosStore } from './todoStore.js'; + +export default function TodosApp() { + const todos = useSyncExternalStore(todosStore.subscribe, todosStore.getSnapshot); + return ( + <> + +
+
    + {todos.map(todo => ( +
  • {todo.text}
  • + ))} +
+ + ); +} ``` -
+```js todoStore.js +// This is an example of a third-party store +// that you might need to integrate with React. - +// If your app is fully built with React, +// we recommend using React state instead. + +let nextId = 0; +let todos = [{ id: nextId++, text: 'Todo #1' }]; +let listeners = []; + +export const todosStore = { + addTodo() { + todos = [...todos, { id: nextId++, text: 'Todo #' + nextId }] + emitChange(); + }, + subscribe(listener) { + listeners = [...listeners, listener]; + return () => { + listeners = listeners.filter(l => l !== listener); + }; + }, + getSnapshot() { + return todos; + } +}; + +function emitChange() { + for (let listener of listeners) { + listener(); + } +} +``` + + + + + +When possible, we recommend to use the built-in React state with [`useState`](/apis/react/useState) and [`useReducer`](/apis/react/useReducer) instead. The `useExternalSyncStore` API is mostly useful if you need to integrate with existing non-React code. + + + +--- + +### Subscribing to a browser API {/*subscribing-to-a-browser-api*/} + +Another reason to add `useSyncExternalStore` is when you want to subscribe to some value exposed by the browser that changes over time. For example, suppose that you want your component to display whether the network connection is active. The browser exposes this information via a property called [`navigator.onLine`.](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/onLine) This value can change over time without React's knowledge, so you need to read it with `useSyncExternalStore`. + +```js +import { useSyncExternalStore } from 'react'; + +function ChatIndicator() { + const isOnline = useSyncExternalStore(subscribe, getSnapshot); + // ... +} +``` + +To implement the `getSnapshot` function, read the current value from the browser API: + +```js +function getSnapshot() { + return navigator.onLine; +} +``` + +Next, you need to implement the `subscribe` function. For example, when `navigator.onLine` changes, the browser fires the [`online`](https://developer.mozilla.org/en-US/docs/Web/API/Window/online_event) and [`offline`](https://developer.mozilla.org/en-US/docs/Web/API/Window/offline_event) events on the `window` object. You need to subscribe the `callback` argument to the corresponding events, and then return a function that cleans up the subscriptions: + +```js +function subscribe(callback) { + window.addEventListener('online', callback); + window.addEventListener('offline', callback); + return () => { + window.removeEventListener('online', callback); + window.removeEventListener('offline', callback); + }; +} +``` + +Now React knows how to read the value from the external `navigator.onLine` API and how to subscribe to its changes. Try to disconnect your device from the network and notice that the component re-renders in response: + + + +```js +import { useSyncExternalStore } from 'react'; + +export default function ChatIndicator() { + const isOnline = useSyncExternalStore(subscribe, getSnapshot); + return

{isOnline ? '✅ Online' : '❌ Disconnected'}

; +} + +function getSnapshot() { + return navigator.onLine; +} + +function subscribe(callback) { + window.addEventListener('online', callback); + window.addEventListener('offline', callback); + return () => { + window.removeEventListener('online', callback); + window.removeEventListener('offline', callback); + }; +} +``` + +
+ +--- + +### Extracting the logic to a custom Hook {/*extracting-the-logic-to-a-custom-hook*/} + +Usually you won't write `useSyncExternalStore` directly in your components. Instead, you'll typically call it from your own custom Hook. This lets you use the same external store from different components. + +For example, this custom `useOnlineStatus` Hook tracks whether the network is online: + +```js {3,6} +import { useSyncExternalStore } from 'react'; + +export function useOnlineStatus() { + const isOnline = useSyncExternalStore(subscribe, getSnapshot); + return isOnline; +} + +function getSnapshot() { + // ... +} + +function subscribe(callback) { + // ... +} +``` + +Now different components can call `useOnlineStatus` without repeating the underlying implementation: + + + +```js +import { useOnlineStatus } from './useOnlineStatus.js'; + +function StatusBar() { + const isOnline = useOnlineStatus(); + return

{isOnline ? '✅ Online' : '❌ Disconnected'}

; +} + +function SaveButton() { + const isOnline = useOnlineStatus(); + + function handleSaveClick() { + console.log('✅ Progress saved'); + } + + return ( + + ); +} + +export default function App() { + return ( + <> + + + + ); +} +``` + +```js useOnlineStatus.js +import { useSyncExternalStore } from 'react'; + +export function useOnlineStatus() { + const isOnline = useSyncExternalStore(subscribe, getSnapshot); + return isOnline; +} + +function getSnapshot() { + return navigator.onLine; +} + +function subscribe(callback) { + window.addEventListener('online', callback); + window.addEventListener('offline', callback); + return () => { + window.removeEventListener('online', callback); + window.removeEventListener('offline', callback); + }; +} +``` + +
+ +--- + +### Adding support for server rendering {/*adding-support-for-server-rendering*/} + +If your React app uses [server rendering,](/apis/react-dom/server) your React components will also run outside the browser environment to generate the initial HTML. This creates a few challenges when connecting to an external store: + +- If you're connecting to a browser-only API, it won't work because it does not exist on the server. +- If you're connecting to a third-party data store, you'll need its data to match between the server and client. + +To solve these issues, pass a `getServerSnapshot` function as the third argument to `useSyncExternalStore`: + +```js {4,12-14} +import { useSyncExternalStore } from 'react'; + +export function useOnlineStatus() { + const isOnline = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot); + return isOnline; +} + +function getSnapshot() { + return navigator.onLine; +} + +function getServerSnapshot() { + return true; // Always show "Online" for server-generated HTML +} + +function subscribe(callback) { + // ... +} +``` + +The `getServerSnapshot` function is similar to `getSnapshot`, but it runs only in two situations: + +- It runs on the server when generating the HTML. +- It runs on the client during [hydration](/apis/react-dom/client/hydrateRoot), i.e. when React takes the server HTML and makes it interactive. + +This lets you provide the initial snapshot value which will be used before the app becomes interactive. If there is no meaningful initial value for the server rendering, you can [force the component to render only on the client.](/apis/react/Suspense#providing-a-fallback-for-server-errors-and-server-only-content) + + + +Make sure that `getServerSnapshot` returns the same exact data on the initial client render as it returned on the server. For example, if `getServerSnapshot` returned some prepopulated store content on the server, you need to transfer this content to the client. One common way to do this is to emit a `