diff --git a/src/content/reference/react/startTransition.md b/src/content/reference/react/startTransition.md index 3b1defd2472..5956bb070be 100644 --- a/src/content/reference/react/startTransition.md +++ b/src/content/reference/react/startTransition.md @@ -4,10 +4,10 @@ title: startTransition -`startTransition` lets you update the state without blocking the UI. +`startTransition` lets you render a part of the UI in the background. ```js -startTransition(scope) +startTransition(action) ``` @@ -18,7 +18,7 @@ startTransition(scope) ## Reference {/*reference*/} -### `startTransition(scope)` {/*starttransitionscope*/} +### `startTransition(action)` {/*starttransition*/} The `startTransition` function lets you mark a state update as a Transition. @@ -41,7 +41,7 @@ function TabContainer() { #### Parameters {/*parameters*/} -* `scope`: A function that updates some state by calling one or more [`set` functions.](/reference/react/useState#setstate) React immediately calls `scope` with no arguments and marks all state updates scheduled synchronously during the `scope` function call as Transitions. They will be [non-blocking](/reference/react/useTransition#marking-a-state-update-as-a-non-blocking-transition) and [will not display unwanted loading indicators.](/reference/react/useTransition#preventing-unwanted-loading-indicators) +* `action`: A function that updates some state by calling one or more [`set` functions](/reference/react/useState#setstate). React calls `action` immediately with no parameters and marks all state updates scheduled synchronously during the `action` function call as Transitions. Any async calls awaited in the `action` will be included in the transition, but currently require wrapping any `set` functions after the `await` in an additional `startTransition` (see [Troubleshooting](#react-doesnt-treat-my-state-update-after-await-as-a-transition)). State updates marked as Transitions will be [non-blocking](#marking-a-state-update-as-a-non-blocking-transition) and [will not display unwanted loading indicators.](#preventing-unwanted-loading-indicators). #### Returns {/*returns*/} @@ -53,13 +53,15 @@ function TabContainer() { * You can wrap an update into a Transition only if you have access to the `set` function of that state. If you want to start a Transition in response to some prop or a custom Hook return value, try [`useDeferredValue`](/reference/react/useDeferredValue) instead. -* The function you pass to `startTransition` must be synchronous. React immediately executes this function, marking all state updates that happen while it executes as Transitions. If you try to perform more state updates later (for example, in a timeout), they won't be marked as Transitions. +* The function you pass to the of `startTransition` is called immediately, marking all state updates that happen while it executes as Transitions. If you try to perform state updates in a `setTimeout`, for example, they won't be marked as Transitions. + +* You must wrap any state updates after any async requests in another `startTransition` to mark them as Transitions. This is a known limitation that we will fix in the future (see [Troubleshooting](#react-doesnt-treat-my-state-update-after-await-as-a-transition)). * A state update marked as a Transition will be interrupted by other state updates. For example, if you update a chart component inside a Transition, but then start typing into an input while the chart is in the middle of a re-render, React will restart the rendering work on the chart component after handling the input state update. * Transition updates can't be used to control text inputs. -* If there are multiple ongoing Transitions, React currently batches them together. This is a limitation that will likely be removed in a future release. +* If there are multiple ongoing Transitions, React currently batches them together. This is a limitation that may be removed in a future release. --- diff --git a/src/content/reference/react/useTransition.md b/src/content/reference/react/useTransition.md index f3e599baa04..7c019bc1626 100644 --- a/src/content/reference/react/useTransition.md +++ b/src/content/reference/react/useTransition.md @@ -4,7 +4,7 @@ title: useTransition -`useTransition` is a React Hook that lets you update the state without blocking the UI. +`useTransition` is a React Hook that lets you render a part of the UI in the background. ```js const [isPending, startTransition] = useTransition() @@ -42,13 +42,13 @@ function TabContainer() { `useTransition` returns an array with exactly two items: 1. The `isPending` flag that tells you whether there is a pending Transition. -2. The [`startTransition` function](#starttransition) that lets you mark a state update as a Transition. +2. The [`startTransition` function](#starttransition) that lets you mark updates as a Transition. --- -### `startTransition` function {/*starttransition*/} +### `startTransition(action)` {/*starttransition*/} -The `startTransition` function returned by `useTransition` lets you mark a state update as a Transition. +The `startTransition` function returned by `useTransition` lets you mark a updates as a Transition. ```js {6,8} function TabContainer() { @@ -64,9 +64,38 @@ function TabContainer() { } ``` + +#### Functions called in `startTransition` are called "Actions". {/*functions-called-in-starttransition-are-called-actions*/} + +The function passed to `startTransition` is called an "Action". By convention, any callback called inside `startTransition` (such as a callback prop) should be named `action` or include the "Action" suffix: + +```js {1,9} +function SubmitButton({ submitAction }) { + const [isPending, startTransition] = useTransition(); + + return ( + + ); +} + +``` + + + + + #### Parameters {/*starttransition-parameters*/} -* `scope`: A function that updates some state by calling one or more [`set` functions.](/reference/react/useState#setstate) React immediately calls `scope` with no parameters and marks all state updates scheduled synchronously during the `scope` function call as Transitions. They will be [non-blocking](#marking-a-state-update-as-a-non-blocking-transition) and [will not display unwanted loading indicators.](#preventing-unwanted-loading-indicators) +* `action`: A function that updates some state by calling one or more [`set` functions](/reference/react/useState#setstate). React calls `action` immediately with no parameters and marks all state updates scheduled synchronously during the `action` function call as Transitions. Any async calls that are awaited in the `action` will be included in the Transition, but currently require wrapping any `set` functions after the `await` in an additional `startTransition` (see [Troubleshooting](#react-doesnt-treat-my-state-update-after-await-as-a-transition)). State updates marked as Transitions will be [non-blocking](#marking-a-state-update-as-a-non-blocking-transition) and [will not display unwanted loading indicators](#preventing-unwanted-loading-indicators). #### Returns {/*starttransition-returns*/} @@ -78,7 +107,9 @@ function TabContainer() { * You can wrap an update into a Transition only if you have access to the `set` function of that state. If you want to start a Transition in response to some prop or a custom Hook value, try [`useDeferredValue`](/reference/react/useDeferredValue) instead. -* The function you pass to `startTransition` must be synchronous. React immediately executes this function, marking all state updates that happen while it executes as Transitions. If you try to perform more state updates later (for example, in a timeout), they won't be marked as Transitions. +* The function you pass to `startTransition` is called immediately, marking all state updates that happen while it executes as Transitions. If you try to perform state updates in a `setTimeout`, for example, they won't be marked as Transitions. + +* You must wrap any state updates after any async requests in another `startTransition` to mark them as Transitions. This is a known limitation that we will fix in the future (see [Troubleshooting](#react-doesnt-treat-my-state-update-after-await-as-a-transition)). * The `startTransition` function has a stable identity, so you will often see it omitted from Effect dependencies, but including it will not cause the Effect to fire. If the linter lets you omit a dependency without errors, it is safe to do. [Learn more about removing Effect dependencies.](/learn/removing-effect-dependencies#move-dynamic-objects-and-functions-inside-your-effect) @@ -86,20 +117,18 @@ function TabContainer() { * Transition updates can't be used to control text inputs. -* If there are multiple ongoing Transitions, React currently batches them together. This is a limitation that will likely be removed in a future release. - ---- +* If there are multiple ongoing Transitions, React currently batches them together. This is a limitation that may be removed in a future release. ## Usage {/*usage*/} -### Marking a state update as a non-blocking Transition {/*marking-a-state-update-as-a-non-blocking-transition*/} +### Perform non-blocking updates with Actions {/*perform-non-blocking-updates-with-actions*/} -Call `useTransition` at the top level of your component to mark state updates as non-blocking *Transitions*. +Call `useTransition` at the top of your component to create Actions, and access the pending state: ```js [[1, 4, "isPending"], [2, 4, "startTransition"]] -import { useState, useTransition } from 'react'; +import {useState, useTransition} from 'react'; -function TabContainer() { +function CheckoutForm() { const [isPending, startTransition] = useTransition(); // ... } @@ -108,315 +137,459 @@ function TabContainer() { `useTransition` returns an array with exactly two items: 1. The `isPending` flag that tells you whether there is a pending Transition. -2. The `startTransition` function that lets you mark a state update as a Transition. +2. The `startTransition` function that lets you create an Action. -You can then mark a state update as a Transition like this: +To start a Transition, pass a function to `startTransition` like this: -```js {6,8} -function TabContainer() { +```js +import {useState, useTransition} from 'react'; +import {updateQuantity} from './api'; + +function CheckoutForm() { const [isPending, startTransition] = useTransition(); - const [tab, setTab] = useState('about'); + const [quantity, setQuantity] = useState(1); - function selectTab(nextTab) { - startTransition(() => { - setTab(nextTab); + function onSubmit(newQuantity) { + startTransition(async function () { + const savedQuantity = await updateQuantity(newQuantity); + startTransition(() => { + setQuantity(savedQuantity); + }); }); } // ... } ``` -Transitions let you keep the user interface updates responsive even on slow devices. +The function passed to `startTransition` is called the "Action". You can update state and (optionally) perform side effects within an Action, and the work will be done in the background without blocking user interactions on the page. A Transition can include multiple Actions, and while a Transition is in progress, your UI stays responsive. For example, if the user clicks a tab but then changes their mind and clicks another tab, the second click will be immediately handled without waiting for the first update to finish. -With a Transition, your UI stays responsive in the middle of a re-render. For example, if the user clicks a tab but then change their mind and click another tab, they can do that without waiting for the first re-render to finish. +To give the user feedback about in-progress Transitions, to `isPending` state switches to `true` at the first call to `startTransition`, and stays `true` until all Actions complete and the final state is shown to the user. Transitions ensure side effects in Actions to complete in order to [prevent unwanted loading indicators](#preventing-unwanted-loading-indicators), and you can provide immediate feedback while the Transition is in progress with `useOptimistic`. - + -#### Updating the current tab in a Transition {/*updating-the-current-tab-in-a-transition*/} +#### Updating the quantity in an Action {/*updating-the-quantity-in-an-action*/} -In this example, the "Posts" tab is **artificially slowed down** so that it takes at least a second to render. +In this example, the `updateQuantity` function simulates a request to the server to update the item's quantity in the cart. This function is *artificially slowed down* so that it takes at least a second to complete the request. -Click "Posts" and then immediately click "Contact". Notice that this interrupts the slow render of "Posts". The "Contact" tab shows immediately. Because this state update is marked as a Transition, a slow re-render did not freeze the user interface. +Update the quantity multiple times quickly. Notice that the pending "Total" state is shown while any requests are in progress, and the "Total" updates only after the final request is complete. Because the update is in an Action, the "quantity" can continue to be updated while the request is in progress. -```js -import { useState, useTransition } from 'react'; -import TabButton from './TabButton.js'; -import AboutTab from './AboutTab.js'; -import PostsTab from './PostsTab.js'; -import ContactTab from './ContactTab.js'; +```json package.json hidden +{ + "dependencies": { + "react": "beta", + "react-dom": "beta" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test --env=jsdom", + "eject": "react-scripts eject" + } +} +``` -export default function TabContainer() { +```js src/App.js +import { useState, useTransition } from "react"; +import { updateQuantity } from "./api"; +import Item from "./Item"; +import Total from "./Total"; + +export default function App({}) { + const [quantity, setQuantity] = useState(1); const [isPending, startTransition] = useTransition(); - const [tab, setTab] = useState('about'); - function selectTab(nextTab) { - startTransition(() => { - setTab(nextTab); + const updateQuantityAction = async newQuantity => { + // To access the pending state of a transition, + // call startTransition again. + startTransition(async () => { + const savedQuantity = await updateQuantity(newQuantity); + startTransition(() => { + setQuantity(savedQuantity); + }); }); - } + }; return ( - <> - selectTab('about')} - > - About - - selectTab('posts')} - > - Posts (slow) - - selectTab('contact')} - > - Contact - +
+

Checkout

+
- {tab === 'about' && } - {tab === 'posts' && } - {tab === 'contact' && } - + +
); } ``` -```js src/TabButton.js -import { useTransition } from 'react'; +```js src/Item.js +import { startTransition } from "react"; -export default function TabButton({ children, isActive, onClick }) { - if (isActive) { - return {children} +export default function Item({action}) { + function handleChange(event) { + // To expose an action prop, call the callback in startTransition. + startTransition(async () => { + action(event.target.value); + }) } return ( - +
+ Eras Tour Tickets + + +
) } - ``` -```js src/AboutTab.js -export default function AboutTab() { - return ( -

Welcome to my profile!

- ); -} -``` - -```js src/PostsTab.js -import { memo } from 'react'; - -const PostsTab = memo(function PostsTab() { - // Log once. The actual slowdown is inside SlowPost. - console.log('[ARTIFICIALLY SLOW] Rendering 500 '); - - let items = []; - for (let i = 0; i < 500; i++) { - items.push(); - } - return ( -
    - {items} -
- ); +```js src/Total.js +const intl = new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD" }); -function SlowPost({ index }) { - let startTime = performance.now(); - while (performance.now() - startTime < 1) { - // Do nothing for 1 ms per item to emulate extremely slow code - } - +export default function Total({quantity, isPending}) { return ( -
  • - Post #{index + 1} -
  • - ); +
    + Total: + + {isPending ? "🌀 Updating..." : `${intl.format(quantity * 9999)}`} + +
    + ) } - -export default PostsTab; ``` -```js src/ContactTab.js -export default function ContactTab() { - return ( - <> -

    - You can find me online here: -

    -
      -
    • admin@mysite.com
    • -
    • +123456789
    • -
    - - ); +```js src/api.js +export async function updateQuantity(newQuantity) { + return new Promise((resolve, reject) => { + // Simulate a slow network request. + setTimeout(() => { + resolve(newQuantity); + }, 2000); + }); } ``` ```css -button { margin-right: 10px } -b { display: inline-block; margin-right: 10px; } +.item { + display: flex; + align-items: center; + justify-content: start; +} + +.item label { + flex: 1; + text-align: right; +} + +.item input { + margin-left: 4px; + width: 60px; + padding: 4px; +} + +.total { + height: 50px; + line-height: 25px; + display: flex; + align-content: center; + justify-content: space-between; +} ```
    - +This is a basic example to demonstrate how Actions work, but this example does not handle requests completing out of order. When updating the quantity multiple times, it's possible for the previous requests to finish after later requests causing the quantity to update out of order. This is a known limitation that we will fix in the future (see [Troubleshooting](#my-state-updates-in-async-transitions-are-out-of-order) below). -#### Updating the current tab without a Transition {/*updating-the-current-tab-without-a-transition*/} +For common use cases, React provides built-in abstractions such as: +- [`useActionState`](/reference/react/useActionState) +- [`
    ` actions](/reference/react-dom/components/form) +- [Server Actions](/reference/rsc/server-actions) -In this example, the "Posts" tab is also **artificially slowed down** so that it takes at least a second to render. Unlike in the previous example, this state update is **not a Transition.** +These solutions handle request ordering for you. When using Transitions to build your own custom hooks or libraries that manage async state transitions, you have greater control over the request ordering, but you must handle it yourself. -Click "Posts" and then immediately click "Contact". Notice that the app freezes while rendering the slowed down tab, and the UI becomes unresponsive. This state update is not a Transition, so a slow re-render freezed the user interface. + - +#### Updating the quantity without an Action {/*updating-the-users-name-without-an-action*/} -```js -import { useState } from 'react'; -import TabButton from './TabButton.js'; -import AboutTab from './AboutTab.js'; -import PostsTab from './PostsTab.js'; -import ContactTab from './ContactTab.js'; +In this example, the `updateQuantity` function also simulates a request to the server to update the item's quantity in the cart. This function is *artificially slowed down* so that it takes at least a second to complete the request. -export default function TabContainer() { - const [tab, setTab] = useState('about'); +Update the quantity multiple times quickly. Notice that the pending "Total" state is shown while any requests is in progress, but the "Total" updates multiple times for each time the "quantity" was clicked: - function selectTab(nextTab) { - setTab(nextTab); + + +```json package.json hidden +{ + "dependencies": { + "react": "beta", + "react-dom": "beta" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test --env=jsdom", + "eject": "react-scripts eject" } +} +``` + +```js src/App.js +import { useState, useTransition } from "react"; +import { updateQuantity } from "./api"; +import Item from "./Item"; +import Total from "./Total"; + +export default function App({}) { + const [quantity, setQuantity] = useState(1); + const [isPending, setIsPending] = useState(false); + + const onUpdateQuantity = async newQuantity => { + // Manually set the isPending State. + setIsPending(true); + const savedQuantity = await updateQuantity(newQuantity); + setIsPending(false); + setQuantity(savedQuantity); + }; return ( - <> - selectTab('about')} - > - About - - selectTab('posts')} - > - Posts (slow) - - selectTab('contact')} - > - Contact - +
    +

    Checkout

    +
    - {tab === 'about' && } - {tab === 'posts' && } - {tab === 'contact' && } - + +
    ); } -``` -```js src/TabButton.js -import { useTransition } from 'react'; +``` -export default function TabButton({ children, isActive, onClick }) { - if (isActive) { - return {children} +```js src/Item.js +export default function Item({onUpdateQuantity}) { + function handleChange(event) { + onUpdateQuantity(event.target.value); } return ( - +
    + Eras Tour Tickets + + +
    ) } - ``` -```js src/AboutTab.js -export default function AboutTab() { +```js src/Total.js +const intl = new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD" +}); + +export default function Total({quantity, isPending}) { return ( -

    Welcome to my profile!

    - ); +
    + Total: + + {isPending ? "🌀 Updating..." : `${intl.format(quantity * 9999)}`} + +
    + ) } ``` -```js src/PostsTab.js -import { memo } from 'react'; +```js src/api.js +export async function updateQuantity(newQuantity) { + return new Promise((resolve, reject) => { + // Simulate a slow network request. + setTimeout(() => { + resolve(newQuantity); + }, 2000); + }); +} +``` -const PostsTab = memo(function PostsTab() { - // Log once. The actual slowdown is inside SlowPost. - console.log('[ARTIFICIALLY SLOW] Rendering 500 '); +```css +.item { + display: flex; + align-items: center; + justify-content: start; +} - let items = []; - for (let i = 0; i < 500; i++) { - items.push(); - } - return ( -
      - {items} -
    - ); -}); +.item label { + flex: 1; + text-align: right; +} -function SlowPost({ index }) { - let startTime = performance.now(); - while (performance.now() - startTime < 1) { - // Do nothing for 1 ms per item to emulate extremely slow code +.item input { + margin-left: 4px; + width: 60px; + padding: 4px; +} + +.total { + height: 50px; + line-height: 25px; + display: flex; + align-content: center; + justify-content: space-between; +} +``` + +
    + +A common solution to this problem is to prevent the user from making changes while the quantity is updating: + + + +```json package.json hidden +{ + "dependencies": { + "react": "beta", + "react-dom": "beta" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test --env=jsdom", + "eject": "react-scripts eject" } +} +``` + +```js src/App.js +import { useState, useTransition } from "react"; +import { updateQuantity } from "./api"; +import Item from "./Item"; +import Total from "./Total"; + +export default function App({}) { + const [quantity, setQuantity] = useState(1); + const [isPending, setIsPending] = useState(false); + + const onUpdateQuantity = async event => { + const newQuantity = event.target.value; + // Manually set the isPending state. + setIsPending(true); + const savedQuantity = await updateQuantity(newQuantity); + setIsPending(false); + setQuantity(savedQuantity); + }; return ( -
  • - Post #{index + 1} -
  • +
    +

    Checkout

    + +
    + +
    ); } -export default PostsTab; ``` -```js src/ContactTab.js -export default function ContactTab() { +```js src/Item.js +export default function Item({isPending, onUpdateQuantity}) { return ( - <> -

    - You can find me online here: -

    -
      -
    • admin@mysite.com
    • -
    • +123456789
    • -
    - - ); +
    + Eras Tour Tickets + + +
    + ) +} +``` + +```js src/Total.js +const intl = new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD" +}); + +export default function Total({quantity, isPending}) { + return ( +
    + Total: + + {isPending ? "🌀 Updating..." : `${intl.format(quantity * 9999)}`} + +
    + ) +} +``` + +```js src/api.js +export async function updateQuantity(newQuantity) { + return new Promise((resolve, reject) => { + // Simulate a slow network request. + setTimeout(() => { + resolve(newQuantity); + }, 2000); + }); } ``` ```css -button { margin-right: 10px } -b { display: inline-block; margin-right: 10px; } +.item { + display: flex; + align-items: center; + justify-content: start; +} + +.item label { + flex: 1; + text-align: right; +} + +.item input { + margin-left: 4px; + width: 60px; + padding: 4px; +} + +.total { + height: 50px; + line-height: 25px; + display: flex; + align-content: center; + justify-content: space-between; +} ```
    +This solution makes the app feel slow, because the user must wait each time they update the quantity. It's possible to add more complex handling manually to allow the user to interact with the UI while the quantity is updating, but Actions handle this case with a straight-forward built-in API. + --- -### Updating the parent component in a Transition {/*updating-the-parent-component-in-a-transition*/} +### Exposing `action` prop from components {/*exposing-action-props-from-components*/} + +You can expose an `action` prop from a component to allow a parent to call an Action. -You can update a parent component's state from the `useTransition` call, too. For example, this `TabButton` component wraps its `onClick` logic in a Transition: + +For example, this `TabButton` component wraps its `onClick` logic in an `action` prop: ```js {8-10} -export default function TabButton({ children, isActive, onClick }) { +export default function TabButton({ action, children, isActive }) { const [isPending, startTransition] = useTransition(); if (isActive) { return {children} @@ -424,7 +597,7 @@ export default function TabButton({ children, isActive, onClick }) { return ( @@ -777,14 +950,9 @@ export default function AboutTab() { ``` ```js src/PostsTab.js hidden +import {use} from 'react'; import { fetchData } from './data.js'; -// Note: this component is written using an experimental API -// that's not yet available in stable versions of React. - -// For a realistic example you can follow today, try a framework -// that's integrated with Suspense, like Relay or Next.js. - function PostsTab() { const posts = use(fetchData('/posts')); return ( @@ -805,31 +973,6 @@ function Post({ title }) { } export default PostsTab; - -// This is a workaround for a bug to get the demo running. -// TODO: replace with real implementation when the bug is fixed. -function use(promise) { - if (promise.status === 'fulfilled') { - return promise.value; - } else if (promise.status === 'rejected') { - throw promise.reason; - } else if (promise.status === 'pending') { - throw promise; - } else { - promise.status = 'pending'; - promise.then( - result => { - promise.status = 'fulfilled'; - promise.value = result; - }, - reason => { - promise.status = 'rejected'; - promise.reason = reason; - }, - ); - throw promise; - } -} ``` ```js src/ContactTab.js hidden @@ -895,7 +1038,7 @@ b { display: inline-block; margin-right: 10px; }
    -Hiding the entire tab container to show a loading indicator leads to a jarring user experience. If you add `useTransition` to `TabButton`, you can instead indicate display the pending state in the tab button instead. +Hiding the entire tab container to show a loading indicator leads to a jarring user experience. If you add `useTransition` to `TabButton`, you can instead display the pending state in the tab button instead. Notice that clicking "Posts" no longer replaces the entire tab container with a spinner: @@ -914,19 +1057,19 @@ export default function TabContainer() { 🌀 Loading...}> setTab('about')} + action={() => setTab('about')} > About setTab('posts')} + action={() => setTab('posts')} > Posts setTab('contact')} + action={() => setTab('contact')} > Contact @@ -942,7 +1085,7 @@ export default function TabContainer() { ```js src/TabButton.js active import { useTransition } from 'react'; -export default function TabButton({ children, isActive, onClick }) { +export default function TabButton({ action, children, isActive }) { const [isPending, startTransition] = useTransition(); if (isActive) { return {children} @@ -953,7 +1096,7 @@ export default function TabButton({ children, isActive, onClick }) { return (