Skip to content
Dashboard

What’s new in React 19

Director of Content

Explore React 19 and how to start using it on Vercel today.

Link to headingServer Components

index.html
<!DOCTYPE html>
<html>
<body>
<div id="root"></div>
<script src="/static/js/bundle.js"></script>
</body>
</html>

page.jsx
export default async function Page() {
const res = await fetch("https://api.example.com/products");
const products = res.json();
return (
<>
<h1>Products</h1>
{products.map((product) => (
<div key={product.id}>
<h2>{product.title}</h2>
<p>{product.description}</p>
</div>
))}
</>
);
}

Try Server Components

This Next.js App Router template lets you experience Server Components with just a few clicks.

Deploy Now

Link to headingNew directives

Link to headingActions

app.tsx
import { useState } from "react";
export default function TodoApp() {
const [items, setItems] = useState([
{ text: "My first todo" },
]);
async function formAction(formData) {
const newItem = formData.get("item");
// Could make a POST request to the server to save the new item
setItems((items) => [...items, { text: newItem }]);
}
return (
<>
<h1>Todo List</h1>
<form action={formAction}>
<input type="text" name="item" placeholder="Add todo..." />
<button type="submit">Add</button>
</form>
<ul>
{items.map((item, index) => (
<li key={index}>{item.text}</li>
))}
</ul>
</>
);
}

Link to headingServer Actions

actions.ts
'use server'
export async function create() {
// Insert into database
}

todo-list.tsx
"use client";
import { create } from "./actions";
export default function TodoList() {
return (
<>
<h1>Todo List</h1>
<form action={create}>
<input type="text" name="item" placeholder="Add todo..." />
<button type="submit">Add</button>
</form>
</>
);
}

Link to headingNew hooks

Link to headinguseActionState

"use client";
import { useActionState } from "react";
import { createUser } from "./actions";
const initialState = {
message: "",
};
export function Signup() {
const [state, formAction, pending] = useActionState(createUser, initialState);
return (
<form action={formAction}>
<label htmlFor="email">Email</label>
<input type="text" id="email" name="email" required />
{/* ... */}
{state?.message && <p aria-live="polite">{state.message}</p>}
<button aria-disabled={pending} type="submit">
{pending ? "Submitting..." : "Sign up"}
</button>
</form>
);
}

Link to headinguseFormStatus

import { useFormStatus } from "react-dom";
import action from "./actions";
function Submit() {
const status = useFormStatus();
return <button disabled={status.pending}>Submit</button>;
}
export default function App() {
return (
<form action={action}>
<Submit />
</form>
);
}

Link to headinguseOptimistic

"use client";
import { useOptimistic } from "react";
import { send } from "./actions";
export function Thread({ messages }) {
const [optimisticMessages, addOptimisticMessage] = useOptimistic(
messages,
(state, newMessage) => [...state, { message: newMessage }],
);
const formAction = async (formData) => {
const message = formData.get("message") as string;
addOptimisticMessage(message);
await send(message);
};
return (
<div>
{optimisticMessages.map((m, i) => (
<div key={i}>{m.message}</div>
))}
<form action={formAction}>
<input type="text" name="message" />
<button type="submit">Send</button>
</form>
</div>
);
}

Link to headingNew API: use

import { use } from "react";
function Cart({ cartPromise }) {
// `use` will suspend until the promise resolves
const cart = use(cartPromise);
return cart.map((item) => <p key={item.id}>{item.title}</p>);
}
function Page({ cartPromise }) {
return (
/*{ ... }*/
// When `use` suspends in Cart, this Suspense boundary will be shown
<Suspense fallback={<div>Loading...</div>}>
<Cart cartPromise={cartPromise} />
</Suspense>
);
}

Link to headingPreloading resources

// React code
import { prefetchDNS, preconnect, preload, preinit } from "react-dom";
function MyComponent() {
preinit("https://.../path/to/some/script.js", { as: "script" });
preload("https://.../path/to/some/font.woff", { as: "font" });
preload("https://.../path/to/some/stylesheet.css", { as: "style" });
prefetchDNS("https://...");
preconnect("https://...");
}

<!-- Resulting HTML -->
<html>
<head>
<link rel="prefetch-dns" href="https://..." />
<link rel="preconnect" href="https://..." />
<link rel="preload" as="font" href="https://.../path/to/some/font.woff" />
<link
rel="preload"
as="style"
href="https://.../path/to/some/stylesheet.css"
/>
<script async="" src="https://.../path/to/some/script.js"></script>
</head>
<body>
<!-- ... -->
</body>
</html>

Link to headingOther improvements

Link to headingref as a prop

function CustomInput({ placeholder, ref }) {
return <input placeholder={placeholder} ref={ref} />;
}
// ...
<CustomInput ref={ref} />;

Link to headingref callbacks

<input
ref={(ref) => {
// ref created
// Return a cleanup function to reset
// ref when element is removed from DOM.
return () => {
// ref cleanup
};
}}
/>;

Link to headingContext as a provider

const ThemeContext = createContext("");
function App({ children }) {
return <ThemeContext value="dark">{children}</ThemeContext>;
}

Link to headinguseDeferredValue initial value

function Search({ deferredValue }) {
// On initial render the value is ''.
// Then a re-render is scheduled with the deferredValue.
const value = useDeferredValue(deferredValue, "");
return <Results value={value} />;
}

Link to headingDocument metadata support

function BlogPost({ post }) {
return (
<article>
<h1>{post.title}</h1>
<title>{post.title}</title>
<meta name="author" content="Jane Doe" />
<link rel="author" href="https://x.com/janedoe" />
<meta name="keywords" content={post.keywords} />
<p>...</p>
</article>
);
}

Link to headingStylesheet support

function ComponentOne() {
return (
<Suspense fallback="loading...">
<link rel="stylesheet" href="one" precedence="default" />
<link rel="stylesheet" href="two" precedence="high" />
<article>...</article>
</Suspense>
);
}
function ComponentTwo() {
return (
<div>
<p>...</p>
{/* Stylesheet "three" below will be inserted between "one" and "two" */}
<link rel="stylesheet" href="three" precedence="default" />
</div>
);
}

Link to headingAsync scripts support

function Component() {
return (
<div>
<script async={true} src="..." />
// ...
</div>
);
}
function App() {
return (
<html>
<body>
<Component>
// ...
</Component> // Won't duplicate script in the DOM
</body>
</html>
);
}

Link to headingCustom Elements support

Link to headingBetter error reporting

Previously, React would throw the error twice. Once for the original error, then a second time after failing to automatically recover, followed by information about the error.Previously, React would throw the error twice. Once for the original error, then a second time after failing to automatically recover, followed by information about the error.
Previously, React would throw the error twice. Once for the original error, then a second time after failing to automatically recover, followed by information about the error.
In React 19, the error is only displayed once.In React 19, the error is only displayed once.
In React 19, the error is only displayed once.
Example of a hydration error message in React 18.Example of a hydration error message in React 18.
Example of a hydration error message in React 18.
Example of an improved hydration error message in React 19.Example of an improved hydration error message in React 19.
Example of an improved hydration error message in React 19.

Link to headingGetting started with React 19 on Vercel

Astro

Deploy React 19 with Astro.

▲ Deploy

Next.js 15 RC

Deploy React 19 with Next.js 15 RC.

▲ Deploy

Vite

Deploy React 19 with Vite.

▲ Deploy

Waku

Deploy React 19 with Waku.

▲ Deploy

Ready to deploy?