React remains the foundation of modern web development. React 19 is now stable and available on npm, bringing significant changes to how developers build applications.
The release solves long-standing complexity issues in React development. Tasks like fetching product data, syncing user preferences, and updating inventory previously required extensive boilerplate code with useEffect hooks and loading states. React 19's Server Components and built-in data fetching handle these operations automatically.
Early adopters report 40% faster development cycles and 25% smaller bundle sizes. Companies including Airbnb and Netflix have started migrations, with some teams seeing 60% improvements in time-to-interactive metrics.
React 19 introduces automatic batching improvements, enhanced Concurrent Features, and better TypeScript integration. These updates represent the framework's biggest evolution since hooks launched in 2018, benefiting everything from startups to enterprise applications serving millions of users.
What's new in React 19?
The game-changing React compiler
The React Compiler is a new JavaScript compiler introduced in React 19 that automatically optimises your components by:
- Automatically memoising components to avoid unnecessary re-renders
- Tracking reactive values (like props and state) with fine-grained precision
- Removing the need for useMemo, useCallback, and React. memo in many cases
How It works
The React Compiler looks at your component code at build time, understands how values change, and generates optimised output that skips re-rendering parts of your component tree unless needed.
It tracks
- Which props and state does each component and effect depend on
- When values are stable or changing
- What parts of JSX need to be re-rendered
Before React 19 (manual optimisation):
import { useMemo, useCallback } from 'react';
function ExpensiveComponent({ items, onItemClick }) {
// Manual memoization required
const expensiveValue = useMemo(() => {
return items.reduce((sum, item) => sum + item.value, 0);
}, [items]);
// Manual callback memoization
const handleClick = useCallback((id) => {
onItemClick(id);
}, [onItemClick]);
return (
<div>
<p>Total: {expensiveValue}</p>
{items.map(item => (
<button key={item.id} onClick={() => handleClick(item.id)}>
{item.name}
</button>
))}
</div>
);
}
With React 19 compiler (automatic optimisation):
function ExpensiveComponent({ items, onItemClick }) {
// No manual memoization needed - compiler handles it!
const expensiveValue = items.reduce((sum, item) => sum + item.value, 0);
const handleClick = (id) => {
onItemClick(id);
};
return (
<div>
<p>Total: {expensiveValue}</p>
{items.map(item => (
<button key={item.id} onClick={() => handleClick(item.id)}>
{item.name}
</button>
))}
</div>
)
}
Bonus: works with React features
Compatible with:
- Server Components
- Transitions (useTransition)
- Actions and Forms (useActionState, useFormStatus)
- Contexts (<Context> JSX syntax)
Opt-in or on by default?
- The compiler is not yet on by default in all tools.
- You'll need to opt in (for now) via:
- Next.js App Router (automatic soon)
- Vite plugin (when released)
- Babel (with @react/compiler)
The React Compiler is the future of how React apps will be written ,with less boilerplate, better performance, and simpler mental models.
Actions API: simplifying synchronous operations
The Actions API in React 19 is a powerful new feature that simplifies asynchronous operations, especially for form submissions, mutations, and server-side logic ,without needing client-side useEffect, fetch calls, or state management for each action.
It’s a core part of React Server Components, but it works seamlessly with client components too.
What is the Actions API?
The Actions API allows you to define server functions (aka Server Actions) that are directly callable from forms or JS, and React automatically handles:
- POST request submission
- FormData parsing
- Server execution
- Client-side state updates
Example: basic server action with form
1. Define a server action
// app/actions.ts
'use server'
export async function sendMessage(_, formData) {
const name = formData.get('name');
const message = formData.get('message');
// Save to DB or send email...
console.log(Message from ${name}: ${message}`);
return { status: 'ok', message: 'Message sent!' };
}
formData is automatically parsed, no manual wiring.
2. Use it in a client component
'use client'
import { useActionState } from 'react'
import { sendMessage } from './actions'
export default function ContactForm() {
const [state, formAction, isPending] = useActionState( sendMessage,{ status: null, message: });
return (
<form action={formAction}>
<input
name="name"
placeholder="Your name"
required
/>
<textarea
name="message"
placeholder="Your message"
Required
/>
<button
type="submit"
disabled={isPending}
>
{isPending? 'Sending...' : 'Send'}
</button>
{state?.message && <p>{state.message}</p>}
</form>
);
}
1.No API routes
2.No client-side fetch()
3.Clean async form state handling
Use cases
- Login / Signup
- Create/update database entries
- Submitting feedback forms
- File uploads (with FormData)
- Triggering emails/side effects
Advanced: combine with useTransition()
'use client'
const [isPending, startTransition] = useTransition();
function handleSubmit (formData) {
startTransition (async () => {
await sendMessage(null, formData);
});
}
Example: form submission with actions
import { useActionState } from 'react';
async function updateName(prevState, formData) {
const name = formData.get('name');
try {
await fetch('/api/update-name', {
method: 'POST',
body: JSON.stringify({ name }),
});
return { success: true, message: 'Name updated successfully!' };
} catch (error) {
return { success: false, message: 'Failed to update name' };
}
}
function NameForm() {
const [state, formAction, isPending] = useActionState(updateName, null);
return (
<form action={formAction}>
<input name="name" placeholder="Enter your name" />
<button type="submit" disabled={isPending}>
{isPending? 'Updating...' : 'Update Name'}
</button>
{state?.message && (
<p style={{ color: state. success? 'green' : 'red' }}>
{state.message}
</p>
)}
</form>
);
}
Optimistic Updates with Actions:
import { useOptimistic } from 'react';
function TodoList({ todos, addTodo }) {
const [optimisticTodos, addOptimisticTodo]=useOptimistic(todos,
(state, newTodo) => [...state, {...newTodo, pending: true }]
);
}
async function handleAddTodo (formData) {
const title = formData.get('title');
const newTodo = { id: Date.now(), title, completed: false };
// Optimistically add the todo
addOptimisticTodo (newTodo);
// Perform the actual update
await addTodo (newTodo);
return (
<div>
<form action={handleAddTodo}>
<input name="title" placeholder="Add a todo" />
<button type="submit">Add Todo</button>
</form>
<ul>
{optimisticTodos.map(todo => (
<li key={todo.id} style={{ opacity: todo.pending? 0.5: 1 }}>
{todo.title}
</li>
))}
</ul>
</div>
);
}
Enhanced server components
In React 19, Server Components get a major upgrade, making them more powerful, flexible, and production-ready. They are now tightly integrated with React's new features like the Actions API, metadata support, and React Compiler.
What are server components?
Server Components are React components that:
- Run only on the server (never sent to the browser)
- Don’t include JavaScript in the client bundle
- Can fetch data directly (e.g., from a DB, API, filesystem)
- Can be composed with Client Components
They improve performance by reducing bundle size and moving work off the client.
Server Components are one of the biggest changes in React 19, providing a new way to render components on the server and deliver a faster, more efficient user experience. The improvements to React Server Components in version 19 bring significant performance benefits:
- Improved initial page load times: Components render on the server, reducing the JavaScript bundle size sent to clients
- Better SEO: Server-rendered content is immediately available to search engines
- Enhanced performance: Reduced client-side processing leads to faster, more responsive applications
Example: Simple server component
// app/pages/about/page.tsx
export default async function AboutPage() {
const data = await fetch('https://api.example.com/team')
.then(res => res.json());
return (
<>
<title>About Us</title>
<h1>Our Team</h1>
<ul>
{data.members.map(member => (
<li key={member.id}>{member.name}</li>
))}
</ul>
</>
)
}
No client-side JS
Can use async/await
Supports <title> natively
Where to Use Server Components
Use Server Components for:
- Data fetching from DB or APIs
- SEO-friendly content (like blog pages)
- Static or dynamic layouts
- Reducing JavaScript on the client
Composing with client components
- React 19 handles boundaries better:
// Server Component
import ClientWidget from './ClientWidget';
export default async function DashboardPage() {
const stats = await getStats();
return (
<>
<h1>Dashboard</h1>
<ClientWidget stats={stats} />
</>
);
}
ClientWidget is marked with "use client" and can handle interactivity.
Enhanced streaming in React 19
React 19 improves streaming rendering, which means:
Server Components can stream chunks of HTML progressively.
<Suspense> works for async server logic.
Reduces Time-to-First-Byte (TTFB).
<Suspense fallback={<Loading />}>
<ExpensiveServerComponent />
</Suspense>
Security & privacy
Server Components:
- Never expose secrets (run only on the server)
- Keep database credentials, tokens, and private logic safe
- Reduce client bundle attack surface
Requirements
To use Enhanced Server Components in React 19:
- React 19 (of course)
- A server-rendering-aware framework like:
- Next.js (App Router)
- Remix (future)
- Custom setups with React Server Build
React 19’s Server Components enable a leaner, faster, and more scalable React architecture ,especially for large apps, content-heavy sites, and hybrid rendering.
The new "use" hook
- The new use hook in React 19 is a powerful and radical addition that allows you to directly use promises, async data, and context in a synchronous way inside components ,without needing useEffect, useState, or Suspense boundaries everywhere.
- It simplifies how we fetch data, consume context, and handle resources in both Server and Client Components.
What is the ‘use’?
use is a React 19 built-in hook that lets you:
- Await any Promise (e.g., from a fetch or DB call)
- Read Context directly in Server Components
- Await resources like loader(), stream(), or cache entries
Example 1: Async data fetching (server component)
import { use } from "react";
const userPromise = fetch("https://api.example.com/user/123")
.then((res) => res.json());
export default function ProfilePage() {
const user = use(userPromise);
return (
<>
<title>{user.name}</title>
<h1>Welcome, {user.name}</h1>
</>
);
}
React suspends automatically until the promise resolves
No loading states or effects needed unless you want them
Example 2: Reading context in server component
// ThemeContext.ts
export const ThemeContext = createContext("light");
// Layout.tsx (Server Component)
import { use } from "react";
import {ThemeContext } from "./ThemeContext";
export default function Layout({ children }) {
const theme = use (ThemeContext);
return (
<div className={`theme-${theme}`}>
{children}
</div>;
)
}
No useContext() needed
Works only on the server (for now)
Advanced: use() with suspense
<Suspense fallback={<Loading />}>
<ExpensiveComponent />
</Suspense>
Example : ExpensiveComponent.tsx
export default function ExpensiveComponent(){
Const data = use(fetch(‘/api/heavy’)
.then(res => res.json());
return <div>{data.value}</div>;
}
React will suspend ExpensiveComponent until the fetch resolves, showing the fallback in the meantime.
When to use ‘use’
- You’re in a Server Component and want to await data cleanly
- You want declarative loading without useEffect
- You're using a context in a server-rendered layout or page
- You want to simplify the code for loading + rendering
JSX transform improvements
The JSX transform receives major enhancements that make it not only more ergonomic but also faster and smarter during compilation and bundling.
The JSX transform receives significant enhancements in React 19, including:
- Using ref as a prop directly
- Performance improvements in JSX compilation
1. Using ref as a Prop directly (no more forwardRef everywhere)
Before (React 18 and earlier)
To pass a ref to a child component, you needed to wrap it in React.forwardRef:
const MyInput = React.forwardRef((props,ref)=>{
return <input ref={ref} {...props} />;
})
Now in React 19:
You can pass ref directly as a JSX prop, and React automatically handles it via the compiler:
function MyInput(props){
return <input {...props} />;
}
export default function Form(){
Const ref = useRef();
return <MyInout ref={ref} />;
}
React Compiler will rewrite this safely behind the scenes, no need for boilerplate!
New ref Prop usage:
function CustomInput ({ref,...props}) {
return <input ref={ref} {...props} />
}
function App() {
const inputRef = useRef(null);
return (
<div>
{/* ref can now be passed as a regular prop*/}
<CustomInput ref={inputRef} placeholder="Type here…" />
<button onClick={()=>inputRef.current?.focus()}>
Focus Input
</button>
</div>
)
}
Before React 19 (forwardRef required):
import {forwardRef} from 'react'
//Previously required forwardRef wrapper
const CustomInput = forwardref(function CustomInput(props,ref){
return <input ref={ref} {...props} />
});
function App() {
const inputRef = useRef(null);
return (
<div>
<CustomInput ref={inputRef} placeholder=”Type here…” />
<button onClick={()=>inputRef.current?.focus()}>
Focus Input
</button>
</div>
)
}
2. Performance improvements in JSX compilation
JSX in React 19 is now:
- More efficient at runtime (less diffing, fewer allocations)
- Compiled smarter by the React Compiler
- Automatically memoised where possible
Example :
<MyComponent prop1={x} prop2={y} />
If x and y are stable, React skips re-rendering entirely without needing memo().
You write JSX normally, but get the performance of fine-tuned memoisation.
useTransition()
In React 19, adding support for using async functions in transitions to handle pending states, errors, forms, and optimistic updates automatically.
useTransition lets you mark a state update as low-priority. React can then interrupt or delay it to keep the UI responsive.
When you want to defer a heavy update (like filtering a big list) so that more urgent updates (like typing in an input) stay fast and responsive.
List filter With transition
import { useState, useTransition } from "react";
function SearchComponent({ data }) {
const [input, setInput] = useState("");
const [filteredData, setFilteredData] = useState(data);
const [isPending, startTransition] = useTransition();
const handleChange = (e) => {
const value = e.target.value;
setInput(value); // High-priority update
startTransition(() => {
const filtered = data.filter(item => item. includes (value));
setFilteredData(filtered); // Low-priority update
});
};
return (
<>
<input value={input} onChange={handleChange} />
{isPending && <p>Updating list...</p>}
<ul>
{filteredData.map((item, i) => <li key={i}>{item}</li>)}
</ul>
</>
)
}
useTransition() - Defer non-urgent state updates isPending - Shows loading state during transition
startTransition(fn) - Marks a state update as low-priority
In React 19, the compiler optimisations and improved scheduling make useTransition more powerful:
- Better scheduling of concurrent tasks.
- Enhanced responsiveness on slow devices.
- Improved server/client sync with Actions and Transitions.
Compared to Web workers?
No, useTransition doesn’t run code on a separate thread like a Web Worker. It just tells React:
“This update isn’t urgent, schedule it when the main thread isn’t busy.”
If you want true parallelism, use Web Workers ,but combine both for advanced apps.
useActionState()
In React 19, useActionState() is a new hook introduced to work with Server Actions, enabling you to manage the state of a form or async action that is submitted to the server.
It combines server-side actions with client-side state, similar to how you'd manage a form and submission result (success, error, pending) ,all in one place.
Syntax
const [state, formAction, isPending] = useActionState(actionFn, initialState);
actionFn: an async Server Action function (defined using the new async function model).
initialState: default state value (e.g., { message: '' })
state: current state from the action (response).
formAction: submit handler you can use in a <form action={formAction}>.
isPending: boolean indicating if the action is running.
Before React19
// actions.ts (React Server Component file)
"use server";
export async function loginAction (prevState, formData) {
const username = formData.get("username");
const password = formData.get("password");
if (username === "admin" && password === "1234") {
return { success: true, message: "Login successful!" };
} else {
return { success: false, message: "Invalid credentials" };
}
}
After React19
// LoginForm.tsx (Client Component)
"use client";
import { useActionState } from "react";
import { loginAction} from "./actions";
export default function LoginForm() {
const [state, formAction, isPending] = useActionState( loginAction,
{ success: null, message: "" }
);
return (
<form action={formAction} className="login-form">
<input type="text" name="username" placeholder="Username" required />
<input type="password" name="password" placeholder="Password" required />
<button type="submit" disabled={isPending}>
{isPending? "Logging in..." : "Login"}
</button>
{state.message && (
<p className={state.success ? "text-green" : "text-red"}>
{state.message}
</p>
)}
</form>
);
}
useActionState() helps you manage form state + server interaction in a unified way.
Works perfectly with React Server Actions introduced in React 19.
Best used in forms for login, signup, submit, etc. where the backend logic runs on the server.
useFormStatus()
useFormStatus() is a new hook designed to be used inside a form that uses Server Actions (React’s new server-side form handling). It helps track the status of a form submission ,whether it’s pending, successful, or failed.
What is useFormStatus?
It lets you read the submission status of a form from inside the form, which is very helpful for:
- Disabling submit buttons during submission
- Showing loading indicators
- Styling inputs or error conditionally
const status = useFormStatus();
status includes:
- pending: boolean – true when the form is submitting
- data: FormData | undefined – submitted form data (optional)
- method: "POST" | "GET" | undefined – submission method
- action: Function | undefined – the form action
- enctype: string – encoding type
“use client”;
import { useFormStatus } from “react-dom”;
function SubmitButton() {
Const {pending} = useFormStatus();
return (
<button type=”submit” disabled={pending}>
{pending ? “Submitting…” : “Submit”}
</button>
)
}
Full Example with useFormStatus + useActionState
// Server Action
// action.ts (server file)
"use server";
export async function submitFormAction(prevState, formData){
await new Promise ((res) => setTimeout(res, 1000));
return {message: "Form submitted!" };
}
// Client Component
"use client";
import { useActionState, useFormStatus } from "react";
import {submitFormAction } from "./actions";
function SubmitButton() {
const {pending} = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? “Submitting…” : “Submit”}
</button>
)
}
export default function ContactForm() {
Const [state, formAction] = useActionState(submitFormAction,{ Message: " "});
return (
<form action={formAction}
<input type="text" name="name" placeholder="Your name" Required />
<SubmitButton />
{state.message && <p>{state.messgae}</p>}
</form>
)
}