TechStackTutor Logo
HOMEBLOGKIDSABOUT USCONTACT USBOOK DEMO
React.js

React 19: What's New

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.

1. React Server Components (Stable)

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.

2. Actions — Form Handling Without Libraries

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> ); }

3. useActionState Hook

useActionState (replaces the old useFormState) manages the state of an action — pending status, return value, and error handling:

jsx
const [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

4. useOptimistic Hook

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> ); }

5. use() Hook

The new use() hook lets you read a Promise or Context inside a component — including inside conditionals and loops (unlike other hooks):

jsx
import { 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.

6. ref as a Prop (No More forwardRef)

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} /> ));

7. Document Metadata in Components

React 19 natively supports rendering <title>, <meta>, and <link> tags from any component — they are automatically hoisted to the document <head>:

jsx
function 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?

  • Build a full-stack app with Next.js 15 and React 19
  • Deep dive into Server Components and streaming
  • React Query vs native Actions — when to use which
  • Testing React 19 components with React Testing Library
Back to Blog