From 47db3ffea1f9b65b75f87ec99d1c204c98eb3d70 Mon Sep 17 00:00:00 2001 From: Ricky Hanlon Date: Sun, 5 May 2024 14:37:24 -0400 Subject: [PATCH 1/9] Add async transitions to React 19 docs --- src/content/reference/react/useTransition.md | 663 ++++++++++++++++++- 1 file changed, 652 insertions(+), 11 deletions(-) diff --git a/src/content/reference/react/useTransition.md b/src/content/reference/react/useTransition.md index f3e599baa04..ec4d8480dcd 100644 --- a/src/content/reference/react/useTransition.md +++ b/src/content/reference/react/useTransition.md @@ -46,7 +46,7 @@ function TabContainer() { --- -### `startTransition` function {/*starttransition*/} +### `startTransition(fn)` {/*starttransition*/} The `startTransition` function returned by `useTransition` lets you mark a state update as a Transition. @@ -64,9 +64,30 @@ function TabContainer() { } ``` +It can also be used to wrap an async function to create an Action: + +```js {6} +function TabButton({data, setData}) { + const [isPending, startTransition] = useTransition(); + + function updateAction(data) { + startTransition(async () => { + const newData = await updateData(data); + + // Note: currently, an additional startTransition + // is needed after any async requests. See Caveats. + startTransition(() => { + setData(data); + }); + }); + } + // ... +} +``` + #### 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) +* `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. Any async calls awaited in the `scope` will be included in the transition, but currently require wrapping any `set` functions after the request 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 +99,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 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, 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) @@ -88,8 +111,6 @@ function TabContainer() { * If there are multiple ongoing Transitions, React currently batches them together. This is a limitation that will likely 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*/} @@ -411,6 +432,447 @@ b { display: inline-block; margin-right: 10px; } --- +### Creating an Action with async Transitions {/*creating-an-action-with-async-transitions*/} + + + +Async transitions are currently available in React 19 beta, and the latest React Canary. + + + +Async transitions allow you to submit async requests within Transitions to handle errors, show pending states, and [prevent unwanted loading indicators](#preventing-unwanted-loading-indicators). Async transitions also integrate into features like `useOptimistic` and `
` actions. By convention, functions that use async transitions are called "Actions". + +You can create an Action by passing an async function to `startTransition`: + +```js {8,13} +import {updateQuantity} from './api'; + +function CheckoutForm() { + const [isPending, startTransition] = useTransition(); + const [quantity, setQuantity] = useState(1); + + function updateQuantityAction(newQuantity) { + startTransition(async () => { + const savedQuantity = await updateQuantity(newQuantity); + startTransition(() => { + setQuantity(savedQuantity); + }); + }); + } + // ... +} +``` + +Actions let you keep the user interface updates responsive even while requests are in progress. + +With Actions, your UI stays responsive in the middle of a request. For example, if the user updates a quantity multiple times, they can do that without waiting for the first request to finish, and the UI will only update after the final request is complete. + + + +#### Updating the quantity in an Action {/*updating-the-quantity-in-an-action*/} + +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. + +Update the quantity multiple times quickly. Notice that the pending "Total" state is shown while any requests is 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. + + + +```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, startTransition] = useTransition(); + + const updateQuantityAction = event => { + const newQuantity = event.target.value; + // Update the quantity in an async transition. + startTransition(async () => { + const savedQuantity = await updateQuantity(newQuantity); + startTransition(() => { + setQuantity(savedQuantity); + }); + }); + }; + + return ( +
+

Checkout

+ +
+ +
+ ); +} +``` + +```js src/Item.js +export default function Item({action}) { + return ( +
+ 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 +.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). + +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) + +These solutions handle request ordering for you. When using async 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. + + + +#### Updating the quantity without an Action {/*updating-the-users-name-without-an-action*/} + +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. + +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: + + + +```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 ( +
+

Checkout

+ +
+ +
+ ); +} + +``` + +```js src/Item.js +export default function Item({onUpdateQuantity}) { + return ( +
+ 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 +.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; +} +``` + +
+ +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 ( +
+

Checkout

+ +
+ +
+ ); +} + +``` + +```js src/Item.js +export default function Item({isPending, onUpdateQuantity}) { + return ( +
+ 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 +.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 has to wait every 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*/} 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: @@ -1554,7 +2016,7 @@ startTransition(() => { }); ``` -The function you pass to `startTransition` must be synchronous. +The function you pass to `startTransition` must be synchronous, or await an async function. You can't mark an update as a Transition like this: @@ -1578,7 +2040,11 @@ setTimeout(() => { }, 1000); ``` -Similarly, you can't mark an update as a Transition like this: +--- + +### React doesn't treat my state update after `await` as a Transition {/*react-doesnt-treat-my-state-update-after-await-as-a-transition*/} + +When you use `await` inside a `startTransition` function, the state updates that happen after the `await` are not marked as transitions. You must wrap state updates after each `await` in a `startTransition` call: ```js startTransition(async () => { @@ -1591,13 +2057,17 @@ startTransition(async () => { However, this works instead: ```js -await someAsyncFunction(); -startTransition(() => { - // ✅ Setting state *during* startTransition call - setPage('/about'); +startTransition(async () => { + await someAsyncFunction(); + // ✅ Using startTransition *after* await + startTransition(() => { + setPage('/about'); + }); }); ``` +This is a JavaScript limitation due to React losing the scope of the async context. In the future, when [AsyncContext](https://github.com/tc39/proposal-async-context) is available, this limitation will be removed. + --- ### I want to call `useTransition` from outside a component {/*i-want-to-call-usetransition-from-outside-a-component*/} @@ -1640,3 +2110,174 @@ function setState() { } } ``` + +### My state updates in async Transitions are out of order {/*my-state-updates-in-async-transitions-are-out-of-order*/} + +If you use async Transitions for user events like click, you might see the updates happen out of order. + +In this example, the `updateQuantity` function simulates a request to the server to update the item's quantity in the cart. This function *artificially returns the every other request after the previous* to simulate race conditions for network requests. + +Try updating the quantity once, then update it quickly multiple times. You might see the incorrect total: + + + +```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, startTransition] = useTransition(); + // Store the actual quantity in separate state to show the mismatch. + const [clientQuantity, setClientQuantity] = useState(1); + + const updateQuantityAction = event => { + const newQuantity = event.target.value; + setClientQuantity(newQuantity); + // Update the quantity in an async transition. + startTransition(async () => { + const savedQuantity = await updateQuantity(newQuantity); + startTransition(() => { + setQuantity(savedQuantity); + }); + }); + }; + + return ( +
+

Checkout

+ +
+ +
+ ); +} + +``` + +```js src/Item.js +export default function Item({action}) { + return ( +
+ Eras Tour Tickets + + +
+ ) +} +``` + +```js src/Total.js +const intl = new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD" +}); + +export default function Total({ clientQuantity, savedQuantity, isPending }) { + return ( +
+ Total: +
+
+ {isPending + ? "🌀 Updating..." + : `${intl.format(savedQuantity * 9999)}`} +
+
+ {!isPending && + clientQuantity !== savedQuantity && + `Wrong total, expected: ${intl.format(clientQuantity * 9999)}`} +
+
+
+ ); +} +``` + +```js src/api.js +let firstRequest = true; +export async function updateQuantity(newName) { + return new Promise((resolve, reject) => { + if (firstRequest === true) { + firstRequest = false; + setTimeout(() => { + firstRequest = true; + resolve(newName); + // Simulate every other request being slower + }, 1000); + } else { + setTimeout(() => { + resolve(newName); + }, 50); + } + }); +} +``` + +```css +.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; +} + +.total div { + display: flex; + flex-direction: column; + align-items: flex-end; +} + +.error { + color: red; +} +``` + +
+ + +When clicking multiple times, it's possible for previous requests to finish after later requests. When this happens, React currently has no way to know the intended order. This is because the updates are scheduled asynchronously, and React loses context of the order across the async boundary. + +This is expected. In the future, React can use [AsyncContext](https://github.com/tc39/proposal-async-context) to track the order of async updates. For common use cases, React provides higher-level abstractions like [`useActionState`](/reference/react/useActionState) and [`` actions](/reference/react-dom/components/form) that handle this automatically. For advanced use cases that use async transitions directly, you'll need to implement your own queuing and abort logic to handle this. + + From 6da0f3b67acd9150342446fc58a8b98c6cbc03b3 Mon Sep 17 00:00:00 2001 From: Rick Hanlon Date: Wed, 27 Nov 2024 14:06:19 -0500 Subject: [PATCH 2/9] Updates from feedback --- src/content/reference/react/useTransition.md | 551 ++++--------------- 1 file changed, 108 insertions(+), 443 deletions(-) diff --git a/src/content/reference/react/useTransition.md b/src/content/reference/react/useTransition.md index ec4d8480dcd..26ad21afbe1 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 update without blocking the UI. ```js const [isPending, startTransition] = useTransition() @@ -42,20 +42,20 @@ 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(fn)` {/*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() { const [isPending, startTransition] = useTransition(); const [tab, setTab] = useState('about'); - function selectTab(nextTab) { + function selectTabAction(nextTab) { startTransition(() => { setTab(nextTab); }); @@ -64,7 +64,7 @@ function TabContainer() { } ``` -It can also be used to wrap an async function to create an Action: +The function passed to `startTransition` is called an Action. If the Action is async, React will wait for the async update to finish before transitioning the UI to the updated state: ```js {6} function TabButton({data, setData}) { @@ -85,9 +85,26 @@ function TabButton({data, setData}) { } ``` + +#### By convention, functions inside `startTransition` are called "Actions". {/*by-convention-functions-that-call-transitions-are-called-actions*/} + +The function passed to `startTransition` is called an "Action". By convention, any callback called inside `startTransition` (such as a callback prop) include the "Action" suffix. + +Transitions can include multiple Actions, such as an Action to update a local component, and another Action to navigate to the next route. Transitions support: + +- **Pending states**: Actions provide a pending state that starts at the beginning of the Transition and automatically resets when the final state update is committed. +- **Optimistic updates**: Actions support the new [`useOptimistic`](#new-hook-optimistic-updates) hook so you can show users instant feedback while the Action is in progress. +- **Error handling**: Actions provide error handling so you can display Error Boundaries when an Action fails, and revert optimistic updates to their original value automatically. +- **Forms**: `` elements now support passing functions to the `action` and `formAction` props. Passing functions to the `action` props use Actions by default and reset the form automatically after submission. + + + + + + #### 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. Any async calls awaited in the `scope` will be included in the transition, but currently require wrapping any `set` functions after the request 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). +* `action`: A function that updates some state by calling one or more [`set` functions](/reference/react/useState#setstate). React immediately calls `action` 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 request 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*/} @@ -113,14 +130,14 @@ function TabButton({data, setData}) { ## 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(); // ... } @@ -129,330 +146,20 @@ 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. - -You can then mark a state update as a Transition like this: - -```js {6,8} -function TabContainer() { - const [isPending, startTransition] = useTransition(); - const [tab, setTab] = useState('about'); - - function selectTab(nextTab) { - startTransition(() => { - setTab(nextTab); - }); - } - // ... -} -``` - -Transitions let you keep the user interface updates responsive even on slow devices. - -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. - - - -#### Updating the current tab in a Transition {/*updating-the-current-tab-in-a-transition*/} - -In this example, the "Posts" tab is **artificially slowed down** so that it takes at least a second to render. - -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. - - - -```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'; - -export default function TabContainer() { - const [isPending, startTransition] = useTransition(); - const [tab, setTab] = useState('about'); - - function selectTab(nextTab) { - startTransition(() => { - setTab(nextTab); - }); - } - - return ( - <> - selectTab('about')} - > - About - - selectTab('posts')} - > - Posts (slow) - - selectTab('contact')} - > - Contact - -
- {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} - } - return ( - - ) -} - -``` - -```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} -
- ); -}); - -function SlowPost({ index }) { - let startTime = performance.now(); - while (performance.now() - startTime < 1) { - // Do nothing for 1 ms per item to emulate extremely slow code - } +2. The `startTransition` function that lets you create an Action. - return ( -
  • - Post #{index + 1} -
  • - ); -} - -export default PostsTab; -``` - -```js src/ContactTab.js -export default function ContactTab() { - return ( - <> -

    - You can find me online here: -

    -
      -
    • admin@mysite.com
    • -
    • +123456789
    • -
    - - ); -} -``` - -```css -button { margin-right: 10px } -b { display: inline-block; margin-right: 10px; } -``` - -
    - - - -#### Updating the current tab without a Transition {/*updating-the-current-tab-without-a-transition*/} - -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.** - -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. - - +To start a transition, pass a function to `startTransition` like this: ```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'; - -export default function TabContainer() { - const [tab, setTab] = useState('about'); - - function selectTab(nextTab) { - setTab(nextTab); - } - - return ( - <> - selectTab('about')} - > - About - - selectTab('posts')} - > - Posts (slow) - - selectTab('contact')} - > - Contact - -
    - {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} - } - return ( - - ) -} - -``` - -```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} -
    - ); -}); - -function SlowPost({ index }) { - let startTime = performance.now(); - while (performance.now() - startTime < 1) { - // Do nothing for 1 ms per item to emulate extremely slow code - } - - return ( -
  • - Post #{index + 1} -
  • - ); -} - -export default PostsTab; -``` - -```js src/ContactTab.js -export default function ContactTab() { - return ( - <> -

    - You can find me online here: -

    -
      -
    • admin@mysite.com
    • -
    • +123456789
    • -
    - - ); -} -``` - -```css -button { margin-right: 10px } -b { display: inline-block; margin-right: 10px; } -``` - -
    - - - -
    - ---- - -### Creating an Action with async Transitions {/*creating-an-action-with-async-transitions*/} - - - -Async transitions are currently available in React 19 beta, and the latest React Canary. - - - -Async transitions allow you to submit async requests within Transitions to handle errors, show pending states, and [prevent unwanted loading indicators](#preventing-unwanted-loading-indicators). Async transitions also integrate into features like `useOptimistic` and `` actions. By convention, functions that use async transitions are called "Actions". - -You can create an Action by passing an async function to `startTransition`: - -```js {8,13} +import {useState, useTransition} from 'react'; import {updateQuantity} from './api'; function CheckoutForm() { const [isPending, startTransition] = useTransition(); const [quantity, setQuantity] = useState(1); - function updateQuantityAction(newQuantity) { - startTransition(async () => { + function onSubmit(newQuantity) { + startTransition(async function () { const savedQuantity = await updateQuantity(newQuantity); startTransition(() => { setQuantity(savedQuantity); @@ -463,15 +170,17 @@ function CheckoutForm() { } ``` -Actions let you keep the user interface updates responsive even while requests are in progress. +The function passed to `startTransition` is called the "Action". In Actions, you can perform side effects and update state within a Transition, without blocking the UI. A Transition can include multiple Actions, and while the Transition is in progress, 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. But if the Action is performing a side effect, such as submitting a form, React will wait for the side effect to finish before changing the tab. -With Actions, your UI stays responsive in the middle of a request. For example, if the user updates a quantity multiple times, they can do that without waiting for the first request to finish, and the UI will only update after the final request is complete. +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 wait for 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 quantity in an Action {/*updating-the-quantity-in-an-action*/} -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. +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. Update the quantity multiple times quickly. Notice that the pending "Total" state is shown while any requests is 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. @@ -502,9 +211,9 @@ export default function App({}) { const [quantity, setQuantity] = useState(1); const [isPending, startTransition] = useTransition(); - const updateQuantityAction = event => { - const newQuantity = event.target.value; - // Update the quantity in an async transition. + const updateQuantityAction = newQuantity => { + // To access the pending state of a transition, + // call startTransition again. startTransition(async () => { const savedQuantity = await updateQuantity(newQuantity); startTransition(() => { @@ -525,14 +234,22 @@ export default function App({}) { ``` ```js src/Item.js +import { startTransition } from "react"; + 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 @@ -606,7 +323,7 @@ For common use cases, React provides built-in abstractions such as: - [`` actions](/reference/react-dom/components/form) - [Server Actions](/reference/rsc/server-actions) -These solutions handle request ordering for you. When using async 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. +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. @@ -643,8 +360,7 @@ export default function App({}) { const [quantity, setQuantity] = useState(1); const [isPending, setIsPending] = useState(false); - const onUpdateQuantity = async event => { - const newQuantity = event.target.value; + const onUpdateQuantity = async newQuantity => { // Manually set the isPending State. setIsPending(true); const savedQuantity = await updateQuantity(newQuantity); @@ -666,13 +382,16 @@ export default function App({}) { ```js src/Item.js export default function Item({onUpdateQuantity}) { + function handleChange(event) { + onUpdateQuantity(event.target.value); + } return (
    Eras Tour Tickets @@ -873,12 +592,15 @@ This solution makes the app feel slow, because the user has to wait every time t --- -### 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 update a parent component's state from the `useTransition` call, too. For example, this `TabButton` component wraps its `onClick` logic in a Transition: +You can expose an `action` prop from a component to allow a parent to call an Action. + + +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} @@ -886,7 +608,7 @@ export default function TabButton({ children, isActive, onClick }) { return ( @@ -1239,14 +961,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 ( @@ -1267,31 +984,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 @@ -1376,19 +1068,19 @@ export default function TabContainer() { 🌀 Loading...}> setTab('about')} + action={() => setTab('about')} > About setTab('posts')} + action={() => setTab('posts')} > Posts setTab('contact')} + action={() => setTab('contact')} > Contact @@ -1404,7 +1096,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} @@ -1415,7 +1107,7 @@ export default function TabButton({ children, isActive, onClick }) { return ( + ); +} +``` From a4f87b42167e86d678041d35ff11f008dc449543 Mon Sep 17 00:00:00 2001 From: Rick Hanlon Date: Mon, 2 Dec 2024 10:41:42 -0500 Subject: [PATCH 4/9] grammar --- src/content/reference/react/useTransition.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/content/reference/react/useTransition.md b/src/content/reference/react/useTransition.md index 55f765f79c7..34a011bef0a 100644 --- a/src/content/reference/react/useTransition.md +++ b/src/content/reference/react/useTransition.md @@ -163,7 +163,7 @@ function CheckoutForm() { The function passed to `startTransition` is called the "Action". In Actions, you can perform side effects and update state within a Transition, without blocking the UI. A Transition can include multiple Actions, and while the Transition is in progress, 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. But if the Action is performing a side effect, such as submitting a form, React will wait for the side effect to finish before changing the tab. +For example, if the user clicks a tab but then changes their mind and clicks another tab, they can do that without waiting for the first re-render to finish. But if the Action is performing a side effect, such as submitting a form, React will wait for the side effect to finish before changing the tab. 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 wait for 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`. From d51a22a603af62bb64eee48bd238633205103ea9 Mon Sep 17 00:00:00 2001 From: Rick Hanlon Date: Mon, 2 Dec 2024 11:52:52 -0500 Subject: [PATCH 5/9] Add startTranstion API --- src/content/reference/react/startTransition.md | 12 +++++++----- src/content/reference/react/useTransition.md | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/content/reference/react/startTransition.md b/src/content/reference/react/startTransition.md index 3b1defd2472..4af2ee84782 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 update without blocking the UI. ```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 immediately calls `action` 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 request 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,7 +53,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 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`, 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. diff --git a/src/content/reference/react/useTransition.md b/src/content/reference/react/useTransition.md index 34a011bef0a..5a8f433ab4a 100644 --- a/src/content/reference/react/useTransition.md +++ b/src/content/reference/react/useTransition.md @@ -107,7 +107,7 @@ function SubmitButton({ submitAction }) { * 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 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, 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`, 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)). From 4b4a8e076fdd0e357a09ac85cb5041e0d09fb9d8 Mon Sep 17 00:00:00 2001 From: Ricky Date: Wed, 4 Dec 2024 15:17:30 -0500 Subject: [PATCH 6/9] Apply suggestions from code review Co-authored-by: Noah Lemen --- src/content/reference/react/startTransition.md | 4 ++-- src/content/reference/react/useTransition.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/content/reference/react/startTransition.md b/src/content/reference/react/startTransition.md index 4af2ee84782..32b4c8eb31e 100644 --- a/src/content/reference/react/startTransition.md +++ b/src/content/reference/react/startTransition.md @@ -41,7 +41,7 @@ function TabContainer() { #### Parameters {/*parameters*/} -* `action`: A function that updates some state by calling one or more [`set` functions](/reference/react/useState#setstate). React immediately calls `action` 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 request 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). +* `action`: A function that updates some state by calling one or more [`set` functions](/reference/react/useState#setstate). React immediately calls `action` 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,7 +53,7 @@ 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 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`, 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)). diff --git a/src/content/reference/react/useTransition.md b/src/content/reference/react/useTransition.md index 5a8f433ab4a..f31d7fbb32d 100644 --- a/src/content/reference/react/useTransition.md +++ b/src/content/reference/react/useTransition.md @@ -95,7 +95,7 @@ function SubmitButton({ submitAction }) { #### Parameters {/*starttransition-parameters*/} -* `action`: A function that updates some state by calling one or more [`set` functions](/reference/react/useState#setstate). React immediately calls `action` 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 request 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). +* `action`: A function that updates some state by calling one or more [`set` functions](/reference/react/useState#setstate). React immediately calls `action` 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 {/*starttransition-returns*/} @@ -107,7 +107,7 @@ function SubmitButton({ submitAction }) { * 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 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`, 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)). From 910901edfe881feb255dcb72e2b563083230c01f Mon Sep 17 00:00:00 2001 From: Rick Hanlon Date: Wed, 4 Dec 2024 15:49:01 -0500 Subject: [PATCH 7/9] Updated --- .../reference/react/startTransition.md | 4 ++-- src/content/reference/react/useTransition.md | 22 ++++++++----------- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/src/content/reference/react/startTransition.md b/src/content/reference/react/startTransition.md index 32b4c8eb31e..7d211754e1d 100644 --- a/src/content/reference/react/startTransition.md +++ b/src/content/reference/react/startTransition.md @@ -4,7 +4,7 @@ title: startTransition -`startTransition` lets you update without blocking the UI. +`startTransition` lets you render a part of the UI in the background. ```js startTransition(action) @@ -61,7 +61,7 @@ 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. --- diff --git a/src/content/reference/react/useTransition.md b/src/content/reference/react/useTransition.md index f31d7fbb32d..c0e3881e1d3 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 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() @@ -95,7 +95,7 @@ function SubmitButton({ submitAction }) { #### Parameters {/*starttransition-parameters*/} -* `action`: A function that updates some state by calling one or more [`set` functions](/reference/react/useState#setstate). React immediately calls `action` 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). +* `action`: A function that updates some state by calling one or more [`set` functions](/reference/react/useState#setstate). React immediately calls `action` 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 {/*starttransition-returns*/} @@ -107,7 +107,7 @@ function SubmitButton({ submitAction }) { * 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 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. +* 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)). @@ -117,7 +117,7 @@ function SubmitButton({ submitAction }) { * 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*/} @@ -161,9 +161,7 @@ function CheckoutForm() { } ``` -The function passed to `startTransition` is called the "Action". In Actions, you can perform side effects and update state within a Transition, without blocking the UI. A Transition can include multiple Actions, and while the Transition is in progress, your UI stays responsive in the middle of a re-render. - -For example, if the user clicks a tab but then changes their mind and clicks another tab, they can do that without waiting for the first re-render to finish. But if the Action is performing a side effect, such as submitting a form, React will wait for the side effect to finish before changing the tab. +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 in the middle of the Actions. For example, if the user clicks a tab but then changes their mind and clicks another tab, the second click will be immediately handled waiting for the first update 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 wait for 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`. @@ -202,7 +200,7 @@ export default function App({}) { const [quantity, setQuantity] = useState(1); const [isPending, startTransition] = useTransition(); - const updateQuantityAction = newQuantity => { + const updateQuantityAction = async newQuantity => { // To access the pending state of a transition, // call startTransition again. startTransition(async () => { @@ -1663,9 +1661,7 @@ startTransition(() => { }); ``` -The function you pass to `startTransition` must be synchronous, or await an async function. - -You can't mark an update as a Transition like this: +The function you pass to `startTransition` must be synchronous. You can't mark an update as a Transition like this: ```js startTransition(() => { @@ -1758,7 +1754,7 @@ function setState() { } ``` -### My state updates in async Transitions are out of order {/*my-state-updates-in-async-transitions-are-out-of-order*/} +### My state updates in Transitions are out of order {/*my-state-updates-in-transitions-are-out-of-order*/} If you `await` inside `startTransition`, you might see the updates happen out of order. @@ -1934,6 +1930,6 @@ export async function updateQuantity(newName) { When clicking multiple times, it's possible for previous requests to finish after later requests. When this happens, React currently has no way to know the intended order. This is because the updates are scheduled asynchronously, and React loses context of the order across the async boundary. -This is expected. In the future, React can use [AsyncContext](https://github.com/tc39/proposal-async-context) to track the order of async updates. For common use cases, React provides higher-level abstractions like [`useActionState`](/reference/react/useActionState) and [`` actions](/reference/react-dom/components/form) that handle this automatically. For advanced use cases that use async transitions directly, you'll need to implement your own queuing and abort logic to handle this. +This is expected, because Actions within a Transition are not ordered. For common use cases, React provides higher-level abstractions like [`useActionState`](/reference/react/useActionState) and [`` actions](/reference/react-dom/components/form) that handle ordering for you. For advanced use cases, you'll need to implement your own queuing and abort logic to handle this. From b20de24b88d41463319cd6cab7e65b2a6d9c00fa Mon Sep 17 00:00:00 2001 From: Rick Hanlon Date: Wed, 4 Dec 2024 16:04:14 -0500 Subject: [PATCH 8/9] capitalization --- src/content/reference/react/useTransition.md | 28 ++++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/content/reference/react/useTransition.md b/src/content/reference/react/useTransition.md index c0e3881e1d3..01c24151712 100644 --- a/src/content/reference/react/useTransition.md +++ b/src/content/reference/react/useTransition.md @@ -67,11 +67,11 @@ 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) are named `action` or include the "Action" suffix: +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, startTranstion] = useTranstion(); + const [isPending, startTransition] = useTransition(); return (