Skip to content

fix action pattern in useTransition / useOptimistic #7796

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Merged
merged 2 commits into from
Apr 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions src/content/reference/react/useOptimistic.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,9 @@ function Thread({ messages, sendMessageAction }) {
function formAction(formData) {
addOptimisticMessage(formData.get("message"));
formRef.current.reset();
sendMessageAction(formData);
startTransition(async () => {
await sendMessageAction(formData);
});
}
const [optimisticMessages, addOptimisticMessage] = useOptimistic(
messages,
Expand Down Expand Up @@ -108,12 +110,10 @@ export default function App() {
const [messages, setMessages] = useState([
{ text: "Hello there!", sending: false, key: 1 }
]);
function sendMessageAction(formData) {
startTransition(async () => {
const sentMessage = await deliverMessage(formData.get("message"));
startTransition(() => {
setMessages((messages) => [{ text: sentMessage }, ...messages]);
})
async function sendMessageAction(formData) {
const sentMessage = await deliverMessage(formData.get("message"));
startTransition(() => {
setMessages((messages) => [{ text: sentMessage }, ...messages]);
})
}
return <Thread messages={messages} sendMessageAction={sendMessageAction} />;
Expand Down
49 changes: 32 additions & 17 deletions src/content/reference/react/useTransition.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,8 @@ function SubmitButton({ submitAction }) {
<button
disabled={isPending}
onClick={() => {
startTransition(() => {
submitAction();
startTransition(async () => {
await submitAction();
});
}}
>
Expand Down Expand Up @@ -227,9 +227,9 @@ import { startTransition } from "react";

export default function Item({action}) {
function handleChange(event) {
// To expose an action prop, call the callback in startTransition.
// To expose an action prop, await the callback in startTransition.
startTransition(async () => {
action(event.target.value);
await action(event.target.value);
})
}
return (
Expand Down Expand Up @@ -585,19 +585,20 @@ This solution makes the app feel slow, because the user must wait each time they

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}
```js {8-12}
export default function TabButton({ action, children, isActive }) {
const [isPending, startTransition] = useTransition();
if (isActive) {
return <b>{children}</b>
}
return (
<button onClick={() => {
startTransition(() => {
action();
startTransition(async () => {
// await the action that's passed in.
// This allows it to be either sync or async.
await action();
});
}}>
{children}
Expand Down Expand Up @@ -656,10 +657,15 @@ export default function TabButton({ action, children, isActive }) {
if (isActive) {
return <b>{children}</b>
}
if (isPending) {
return <b className="pending">{children}</b>;
}
return (
<button onClick={() => {
startTransition(() => {
action();
<button onClick={async () => {
startTransition(async () => {
// await the action that's passed in.
// This allows it to be either sync or async.
await action();
});
}}>
{children}
Expand Down Expand Up @@ -729,10 +735,19 @@ export default function ContactTab() {
```css
button { margin-right: 10px }
b { display: inline-block; margin-right: 10px; }
.pending { color: #777; }
```

</Sandpack>

<Note>

When exposing an `action` prop from a component, you should `await` it inside the transition.

This allows the `action` callback to be either synchronous or asynchronous without requiring an additional `startTransition` to wrap the `await` in the action.

</Note>

---

### Displaying a pending visual state {/*displaying-a-pending-visual-state*/}
Expand Down Expand Up @@ -804,8 +819,8 @@ export default function TabButton({ action, children, isActive }) {
}
return (
<button onClick={() => {
startTransition(() => {
action();
startTransition(async () => {
await action();
});
}}>
{children}
Expand Down Expand Up @@ -1095,8 +1110,8 @@ export default function TabButton({ action, children, isActive }) {
}
return (
<button onClick={() => {
startTransition(() => {
action();
startTransition(async () => {
await action();
});
}}>
{children}
Expand Down Expand Up @@ -1822,8 +1837,8 @@ import {startTransition} from 'react';
export default function Item({action}) {
function handleChange(e) {
// Update the quantity in an Action.
startTransition(() => {
action(e.target.value);
startTransition(async () => {
await action(e.target.value);
});
}
return (
Expand Down