use, useOptimistic, useActionState & useFormStatus
The newer hooks around forms, server actions and promises
React 19 brings four building blocks that make forms, loading states and “instantly visible” actions considerably shorter – especially in Next.js.
You only need these four once you build forms talking to a server. If anything here moves too fast: no harm done – come back later. For everyday work useState, useEffect and the others from the previous lessons are plenty.
use – waiting for a result without useEffect
So far, loading data meant useEffect plus a state for loading plus one for errors. use turns that around: you hand it a promise and get the value straight back. While it is missing, a SuspenseAn area that shows a placeholder (“loading …”) while something inside it is not ready yet.→ boundary above shows the loading state.
import { use, Suspense } from "react";
function Profile({ userPromise }: { userPromise: Promise<User> }) {
const user = use(userPromise); // pauses until the promise resolves
return <h1>{user.name}</h1>;
}
// The parent takes care of the loading state and errors:
<Suspense fallback={<Skeleton />}>
<Profile userPromise={loadUser(id)} />
</Suspense> Not the component itself. It simply pauses, and the nearest <Suspense> boundary above steps in with its fallback. For errors there is the counterpart: an error boundary.
▸ In more depth: use is allowed inside an if optional
use is the only hook the “top level only” rule does not apply to. It may sit inside conditions and loops. That also holds when you read a ContextA kind of pneumatic tube: a value goes in at the top and can be taken out anywhere below.→ with it:
function Row({ highlight }) {
if (highlight) {
const theme = use(ThemeContext); // with useContext this would be forbidden
return <div style={{ color: theme.accent }}>…</div>;
}
return <div>…</div>;
} use(loadUser(id)) written directly in the component would be an endless loop: every pass creates a new promise to wait on. The promise comes from a Server componentA component that only runs on the server. It cannot use hooks and cannot handle clicks.In Next.js components are server components by default. Adding "use client" at the top of the file makes it a client component.→ or from a cache.
useActionState – form, result and loading state in one
"use client";
const [state, formAction, isPending] = useActionState(
async (previous: State, formData: FormData) => {
const mail = formData.get("mail") as string;
if (!mail.includes("@")) return { error: "Please enter a valid address" };
await signUp(mail);
return { success: true };
},
{ error: null } // starting value
);
return (
<form action={formAction}>
<input name="mail" />
<button disabled={isPending}>{isPending ? "Sending …" : "Sign up"}</button>
{state.error && <p role="alert">{state.error}</p>}
</form>
); const { pending } = useFormStatus() reads the state of the surrounding form. That lets a generic <SubmitButton> disable itself without any props being passed down. Important: the hook has to live in a component inside the <form>, not in the same one.
useOptimistic – show it now, correct it later
const [messages, setMessages] = useState(initial);
const [visible, addOptimistic] = useOptimistic(
messages,
(current: Message[], newText: string) => [
...current,
{ text: newText, sending: true },
]
);
async function send(formData: FormData) {
const text = formData.get("text") as string;
addOptimistic(text); // visible immediately
const saved = await sendToServer(text);
setMessages((m) => [...m, saved]);
} - +
usefor data a server component hands in as a promise - +
useActionStatefor forms with validation and a server response - +
useFormStatusin reusable submit buttons - +
useOptimisticfor likes, comments, toggles – actions that nearly always succeed
- −
useOptimisticfor payments or anything that often fails - −
usewith a promise you recreate while drawing – endless loop - −
useFormStatusin the same component that renders the<form>
These are client hooks ("use client"), but they are built to work with server actions. The action itself may be a "use server" function – then validation runs on the server and the form works even with JavaScript disabled.
Does it stick?
4 questions on this lesson. Wrong answers show up in your stats.
Now write it yourself
After you submit it takes half a second for the entry to appear – which feels sluggish. Show it immediately (greyed out, with a “…”) and replace it once the answer arrives.
- □Right after the click the new text is in the list
- □Once the simulated request finishes it is in there for good
- □Afterwards the entry appears exactly once
import { useOptimistic, useState, useTransition } from "react"; const wait = (ms: number) => new Promise((r) => setTimeout(r, ms)); export default function App() { const [items, setItems] = useState<string[]>(["First"]); const [text, setText] = useState(""); const [, start] = useTransition(); // TODO: derive the optimistic list function send() { if (!text.trim()) return; const value = text; setText(""); start(async () => { // TODO: show it optimistically first await wait(500); setItems((p) => [...p, value]); }); } return ( <div style={{ fontFamily: "system-ui", padding: 16 }}> <input aria-label="text" value={text} onChange={(e) => setText(e.target.value)} />{" "} <button onClick={send}>Send</button> <ul> {items.map((i, idx) => ( <li key={idx}>{i}</li> ))} </ul> </div> ); }