Server Components, Actions, new hooks, and everything that changed
April 30, 2025
6 min read
React 19 is the most significant release since React 18. It stabilizes Server Components, introduces a first-class Actions API for form handling, ships several new hooks, and dramatically simplifies patterns that previously required third-party libraries.
Server Components run on the server at request time — they can fetch data directly, access databases, and never ship their code to the browser:
jsx// app/users/page.tsx — Server Component (default in Next.js App Router) // No "use client" = runs only on the server async function UsersPage() { // Direct database/API call — no useEffect, no loading state const users = await fetch("https://api.example.com/users").then(r => r.json()); return ( <ul> {users.map(u => <li key={u.id}>{u.name}</li>)} </ul> ); } export default UsersPage;
Server Components reduce your JavaScript bundle size — their render logic never reaches the browser. Use them for data-fetching; use Client Components ("use client") for interactivity.
React 19 introduces Actions: async functions you pass directly to <form action={...}>. They handle pending state, errors, and optimistic updates natively:
jsx"use client"; import { useActionState } from "react"; async function submitForm(prevState, formData) { const name = formData.get("name"); const email = formData.get("email"); const res = await fetch("/api/contact", { method: "POST", body: JSON.stringify({ name, email }), headers: { "Content-Type": "application/json" }, }); if (!res.ok) return { error: "Something went wrong." }; return { success: true }; } export default function ContactForm() { const [state, formAction, isPending] = useActionState(submitForm, null); return ( <form action={formAction}> <input name="name" placeholder="Your name" required /> <input name="email" type="email" placeholder="Your email" required /> <button type="submit" disabled={isPending}> {isPending ? "Sending..." : "Send"} </button> {state?.error && <p style={{ color: "red" }}>{state.error}</p>} {state?.success && <p style={{ color: "green" }}>Sent!</p>} </form> ); }
useActionState (replaces the old useFormState) manages the state of an action — pending status, return value, and error handling:
jsxconst [state, dispatch, isPending] = useActionState(action, initialState); // state — the latest return value from the action // dispatch — call this to trigger the action // isPending — true while the action is running
Show an optimistic UI update immediately while waiting for the server to confirm:
jsx"use client"; import { useOptimistic, useState } from "react"; async function addTodo(formData) { await fetch("/api/todos", { method: "POST", body: formData, }); } export default function TodoList({ todos }) { const [optimisticTodos, addOptimisticTodo] = useOptimistic( todos, (state, newTodo) => [...state, { id: Date.now(), text: newTodo, pending: true }] ); async function handleSubmit(formData) { const text = formData.get("text"); addOptimisticTodo(text); // show immediately await addTodo(formData); // confirm with server } return ( <form action={handleSubmit}> <input name="text" placeholder="New todo" /> <button type="submit">Add</button> <ul> {optimisticTodos.map(t => ( <li key={t.id} style={{ opacity: t.pending ? 0.5 : 1 }}> {t.text} </li> ))} </ul> </form> ); }
The new use() hook lets you read a Promise or Context inside a component — including inside conditionals and loops (unlike other hooks):
jsximport { use, Suspense } from "react"; // Read a Promise function UserProfile({ userPromise }) { const user = use(userPromise); // suspends until resolved return <h1>{user.name}</h1>; } // Wrap with Suspense function Page() { const userPromise = fetch("/api/user").then(r => r.json()); return ( <Suspense fallback={<p>Loading...</p>}> <UserProfile userPromise={userPromise} /> </Suspense> ); } // Read Context conditionally (previously impossible) function MyComponent({ shouldRead }) { if (shouldRead) { const theme = use(ThemeContext); // ✅ allowed in React 19 return <div style={{ color: theme.primary }}>Hello</div>; } return null; }
use() is the only hook that can be called inside conditionals and loops. It replaces the need for useContext in most cases and enables new async component patterns.
In React 19, function components can accept ref directly as a prop — forwardRef is no longer needed:
jsx// React 19 — ref is just a prop function Input({ ref, ...props }) { return <input ref={ref} {...props} />; } // Usage const inputRef = useRef(null); <Input ref={inputRef} placeholder="Type here" /> // Before React 19 — needed forwardRef wrapper const Input = forwardRef((props, ref) => ( <input ref={ref} {...props} /> ));
React 19 natively supports rendering <title>, <meta>, and <link> tags from any component — they are automatically hoisted to the document <head>:
jsxfunction BlogPost({ post }) { return ( <article> <title>{post.title} | TechStackTutor</title> <meta name="description" content={post.summary} /> <link rel="canonical" href={`https://techstacktutor.com/blog/${post.slug}`} /> <h1>{post.title}</h1> <p>{post.content}</p> </article> ); }
What's Next?