From 0ebfbff663fbdf5abad24812de55b7b879f9fb76 Mon Sep 17 00:00:00 2001 From: Ajit <47279189+ajits01@users.noreply.github.com> Date: Thu, 10 Apr 2025 23:13:46 +0530 Subject: [PATCH] Add example of `useActionState` handling execution order --- src/content/reference/react/useTransition.md | 159 +++++++++++++++++++ 1 file changed, 159 insertions(+) diff --git a/src/content/reference/react/useTransition.md b/src/content/reference/react/useTransition.md index 6b7c511e7c0..33b1214541a 100644 --- a/src/content/reference/react/useTransition.md +++ b/src/content/reference/react/useTransition.md @@ -1933,3 +1933,162 @@ When clicking multiple times, it's possible for previous requests to finish afte This is expected, because Actions within a Transition do not guarantee execution order. 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. +Example of `useActionState` handling execution order: + + + +```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, useActionState } from "react"; +import { updateQuantity } from "./api"; +import Item from "./Item"; +import Total from "./Total"; + +export default function App({}) { + // Store the actual quantity in separate state to show the mismatch. + const [clientQuantity, setClientQuantity] = useState(1); + const [quantity, updateQuantityAction, isPending] = useActionState( + async (prevState, payload) => { + setClientQuantity(payload); + const savedQuantity = await updateQuantity(payload); + return savedQuantity; // Return the new quantity to update the state + }, + 1 // Initial quantity + ); + + return ( +
+

Checkout

+ +
+ +
+ ); +} + +``` + +```js src/Item.js +import {startTransition} from 'react'; + +export default function Item({action}) { + function handleChange(e) { + // Update the quantity in an Action. + startTransition(() => { + action(e.target.value); + }); + } + 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; +} +``` + +