mirror of
https://github.com/reactjs/react.dev.git
synced 2026-02-21 19:31:57 +00:00
Add example of useActionState handling execution order (#7733)
This commit is contained in:
@@ -1948,3 +1948,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 [`<form>` 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:
|
||||
|
||||
<Sandpack>
|
||||
|
||||
```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 (
|
||||
<div>
|
||||
<h1>Checkout</h1>
|
||||
<Item action={updateQuantityAction}/>
|
||||
<hr />
|
||||
<Total clientQuantity={clientQuantity} savedQuantity={quantity} isPending={isPending} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
```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 (
|
||||
<div className="item">
|
||||
<span>Eras Tour Tickets</span>
|
||||
<label htmlFor="name">Quantity: </label>
|
||||
<input
|
||||
type="number"
|
||||
onChange={handleChange}
|
||||
defaultValue={1}
|
||||
min={1}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
```js src/Total.js
|
||||
const intl = new Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD"
|
||||
});
|
||||
|
||||
export default function Total({ clientQuantity, savedQuantity, isPending }) {
|
||||
return (
|
||||
<div className="total">
|
||||
<span>Total:</span>
|
||||
<div>
|
||||
<div>
|
||||
{isPending
|
||||
? "🌀 Updating..."
|
||||
: `${intl.format(savedQuantity * 9999)}`}
|
||||
</div>
|
||||
<div className="error">
|
||||
{!isPending &&
|
||||
clientQuantity !== savedQuantity &&
|
||||
`Wrong total, expected: ${intl.format(clientQuantity * 9999)}`}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
```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;
|
||||
}
|
||||
```
|
||||
|
||||
</Sandpack>
|
||||
|
||||
Reference in New Issue
Block a user