{
"$type": "site.standard.document",
"bskyPostRef": {
"cid": "bafyreieur6ghexv2jetdb75l5kcj7v7ky5ag5ltjd2vgqyn6xl5lt76ihu",
"uri": "at://did:plc:25rdn5elo5izoxrmtis34zuk/app.bsky.feed.post/3mohzc5622t32"
},
"coverImage": {
"$type": "blob",
"ref": {
"$link": "bafkreiazpjuxwtuqihwwad3ekc7iwnsyecgx2o2fixj5ira4bxyl3qobl4"
},
"mimeType": "image/webp",
"size": 118012
},
"path": "/nayankyada/react-19-new-hooks-in-nextjs-app-router-what-actually-changed-for-me-1d4i",
"publishedAt": "2026-06-17T09:17:26.000Z",
"site": "https://dev.to",
"tags": [
"react19",
"nextjs",
"approuter",
"serveractions"
],
"textContent": "React 19's new hooks — `useFormStatus`, `useActionState`, and `use()` — aren't just quality-of-life additions. In the context of the Next.js App Router and Server Actions, they change the fundamental shape of how I write forms and async UI. Here's what that looks like in practice, without the hype.\n\n## The problem these hooks are solving\n\nBefore React 19, building a form that called a Server Action and reflected loading/error state required a fair amount of ceremony. You'd reach for `useState` + `useTransition`, manually thread the pending flag down to a submit button, and handle error returns with your own convention. It worked, but it was boilerplate you wrote every time.\n\nReact 19 formalises that pattern into three primitives. I'll go through each one with a concrete before/after.\n\n## useActionState replaces the useState + useTransition combo\n\nBefore React 19, a typical contact form Server Action pattern looked like this:\n\n\n\n // app/contact/page.tsx (before React 19)\n 'use client'\n import { useTransition, useState } from 'react'\n import { submitContact } from './actions'\n\n export default function ContactForm() {\n const [error, setError] = useState<string | null>(null)\n const [success, setSuccess] = useState(false)\n const [isPending, startTransition] = useTransition()\n\n function handleSubmit(e: React.FormEvent<HTMLFormElement>) {\n e.preventDefault()\n const formData = new FormData(e.currentTarget)\n startTransition(async () => {\n const result = await submitContact(formData)\n if (result.error) setError(result.error)\n else setSuccess(true)\n })\n }\n\n return (\n <form onSubmit={handleSubmit}>\n {error && <p>{error}</p>}\n {success && <p>Sent!</p>}\n <input name=\"email\" type=\"email\" />\n <button disabled={isPending}>{isPending ? 'Sending…' : 'Send'}</button>\n </form>\n )\n }\n\n\nWith `useActionState` this collapses significantly:\n\n\n\n // app/contact/page.tsx (React 19)\n 'use client'\n import { useActionState } from 'react'\n import { submitContact } from './actions'\n\n type State = { error?: string; success?: boolean }\n const initialState: State = {}\n\n export default function ContactForm() {\n const [state, formAction, isPending] = useActionState(submitContact, initialState)\n\n return (\n <form action={formAction}>\n {state.error && <p>{state.error}</p>}\n {state.success && <p>Sent!</p>}\n <input name=\"email\" type=\"email\" />\n <button disabled={isPending}>{isPending ? 'Sending…' : 'Send'}</button>\n </form>\n )\n }\n\n\nThe Server Action now receives `(prevState: State, formData: FormData)` as its signature. The hook owns the pending flag and state transitions. I no longer have to wire `e.preventDefault()` or `startTransition` manually — the `action` prop on `<form>` handles that natively when given a function.\n\nOne thing worth noting: your Server Action signature _must_ change to accept `prevState` as the first argument when used with `useActionState`. If you're migrating existing actions, that's the breaking change to watch for.\n\n## useFormStatus pushes pending state down without prop drilling\n\nThe submit button sitting inside a form has no way of knowing the form is pending unless you pass the flag down as a prop. `useFormStatus` solves this by reading the parent form's status from context.\n\n\n\n // components/submit-button.tsx\n 'use client'\n import { useFormStatus } from 'react-dom'\n\n export function SubmitButton({ label }: { label: string }) {\n const { pending } = useFormStatus()\n return (\n <button type=\"submit\" disabled={pending}>\n {pending ? 'Working…' : label}\n </button>\n )\n }\n\n\nThis component can now be dropped into any form that uses a Server Action — or `useActionState` — and it automatically reflects pending state. No prop, no context setup, no wiring. It reads the nearest ancestor `<form>` that has a pending action.\n\nThe constraint is real: `useFormStatus` only works inside a component that is _rendered as a child of the form_ , not inside the same component that renders the form. I've seen people trip on this. The fix is always the same — extract the button to its own component.\n\n## use() for async data and context unwrapping\n\n`use()` is the strangest of the three because it does two different things depending on what you pass it.\n\nFor **promises** , it lets you pass a promise from a Server Component down to a Client Component and unwrap it there with Suspense. This is the pattern that replaces some `useEffect`-based data fetching:\n\n\n\n // app/dashboard/page.tsx (Server Component)\n import { Suspense } from 'react'\n import { fetchUserStats } from '@/lib/data'\n import { StatsPanel } from './stats-panel'\n\n export default function DashboardPage() {\n // Not awaited — passes the promise down\n const statsPromise = fetchUserStats()\n return (\n <Suspense fallback={<p>Loading stats…</p>}>\n <StatsPanel promise={statsPromise} />\n </Suspense>\n )\n }\n\n // app/dashboard/stats-panel.tsx (Client Component)\n 'use client'\n import { use } from 'react'\n import type { UserStats } from '@/lib/data'\n\n export function StatsPanel({ promise }: { promise: Promise<UserStats> }) {\n const stats = use(promise) // suspends until resolved\n return <div>{stats.totalOrders} orders</div>\n }\n\n\nThe Server Component starts the fetch, hands off the promise, and the Client Component suspends. The practical benefit over `useEffect` is that the fetch starts server-side and the waterfall is shorter.\n\nFor **context** , `use(MyContext)` is just `useContext(MyContext)` with one meaningful difference: you can call it conditionally. That matters in practice when you have context that only exists in certain render paths and you want to avoid the rules-of-hooks gymnastics.\n\n## What this means for how I structure App Router projects\n\nThese three hooks push me toward a cleaner separation. Server Actions stay as plain async functions with a typed return — no client state logic bleeds in. Client Components that render forms are slimmer because they don't own the pending/error state machinery anymore. And small components like `SubmitButton` can be genuinely reusable without receiving one-off props.\n\nThe shift I haven't fully resolved: `useActionState` pairs state and action together, which is convenient but means the action is tightly coupled to that particular state shape. If the same Server Action is called from two forms with different UX needs, I end up duplicating or wrapping. That's a real trade-off, not a deal-breaker, but worth knowing before you adopt this pattern everywhere.\n\nOverall, these hooks feel like React acknowledging that Server Actions are a first-class pattern and building the client-side ergonomics to match. The before/after diff is substantial enough that I've started migrating existing forms as I touch them.",
"title": "React 19 new hooks in Next.js App Router: what actually changed for me"
}