What’s new in React 19
In this post I want to catalog the new important features of React 19.
React 19 is almost ready for its stable release.
A lot of the features that Next.js currently uses are still marked as experimental in React, and available in the Canary release of React.
RSC, server actions and company have been so much central to every discussion on X that I forgot everything about this is not in the stable version of React yet.
Which is not bad per se. I think the ecosystem can move forward way faster, and in a better direction, by having less big releases and more iterative exploration.
So all I’m going to list is already stuff we use in the latest Next.js version, but I wanted somewhere I could refer to all the time to get a tldr; of them.
Server Components
React Server Components (RSC) is definitely the biggest change in React since hooks were introduced in 2018.
It’s a ground-breaking switch from what we used to do.
Basically React with RSC transforms into a full-stack framework, without needing what frameworks used to provide.
We used to need Next.js or Remix (or others) to provide server-side features to React, otherwise we were limited to using client-side rendering (what happens when you use Vite).
But with RSC, server side rendering is now native to React, so frameworks can now focus on improving other parts of the whole “application built with React” experience.
With server components:
- full HTML is generated server side and shipped directly to the browser, instead of shipping an empty HTML skeleton and having to download all the JavaScript and run a JavaScript application in the browser in order to render the HTML
- data can be fetched on the server and you don’t have to wait for the JavaScript app to be ran in the browser before you can run a
fetch()
call. You fetch data on the server before shipping the fully rendered HTML to the browser. - subsequent navigation between routes can then happen client-side once the JavaScript app is loaded in the browser
Directives
Components in React 19 are now server-rendered by default.
To render a component client-side only, you use a directive at the top of it:
'use client'
Actions (form actions)
Within a component you might need to respond to an event (for example a click) with an event handler that must trigger something on the server.
Very common pattern:
import { useState } from 'react'
export const Demo = () => {
const [fullName, setFullName] = useState('')
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
await fetch('https://api.example.com/submit', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ fullName }),
})
}
return (
<div>
<form onSubmit={handleSubmit}>
<input
type='text'
value={fullName}
onChange={(e) => setFullName(e.target.value)}
/>
<button type='submit'>Submit</button>
</form>
</div>
)
}
Same example, using FormData
, which removes the need to track each individual input field value using a state management tool (useState
in the above example):
import { useRef } from 'react'
export const Demo = () => {
const formRef = useRef<HTMLFormElement>(null)
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
if (formRef.current) {
const formData = new FormData(formRef.current)
await fetch('https://api.example.com/submit', {
method: 'POST',
body: formData,
})
}
}
return (
<div>
<form ref={formRef} onSubmit={handleSubmit}>
<input
type='text'
name='fullName'
/>
<button type='submit'>Submit</button>
</form>
</div>
)
}
The same thing now can be performed using an action:
export const Demo = () => {
async function myFormAction(formData) => {
await fetch('https://api.example.com/submit', {
method: 'POST',
body: formData,
})
}
return (
<div>
<form action={myFormAction}>
<input
type='text'
name='fullName'
/>
<button type='submit'>Submit</button>
</form>
</div>
)
}
Look how much simpler the code is, because now we don’t track the individual input fields state, but also we don’t respond to a browser event directly, and we don’t have to pass the form ref around to keep track of how to access it, because the actions is directly passed the FormData
object.
Server actions
The above action was executed client-side.
Server actions are defined in a separate .ts
file marked with the 'use server'
directive.
This tells React what is in that file can only run on the server, and can be called from a client component:
//actions.ts
'use server'
async function myServerAction(formData) => {
//we are on the server, we can directly
//do something with the form data
}
In a client component:
'use client'
import { myServerAction } from './actions'
export const Demo = () => {
return (
<div>
<form action={myServerAction}>
<input
type='text'
name='fullName'
/>
<button type='submit'>Submit</button>
</form>
</div>
)
}
The useActionState
hook
(in previous canary versions known as useFormState
)
Client components can use this new hook to “peek” into the state of a server action.
We basically pass it the server action, and an initial state object.
What we get back is the state
object, the form action we attach to the form, and the pending
object:
"use client"
import { useActionState } from "react"
import { myServerAction } from './actions'
const initialState = {
message: "",
}
export const Demo = () => {
const [state, formAction, pending] =
useActionState(myServerAction, initialState)
return (
<div>
<form action={formAction}>
<input
type='text'
name='fullName'
/>
{state?.message && <p>{state.message}</p>}
<button
aria-disabled={pending}
type='submit'>
{pending ? "Submitting..." : "Submit"}
</button>
</form>
</div>
)
}
Basically we have a very easy way of handing feedback from the server action (in this case we assume it returns an object with a message
string), and we have an easy way of handling the pending state, so we can disable the button while the action is running.
Stuff we didn’t implement before, but you can see how it’s quick to implement using useActionState
.
The useFormStatus
hook
Similarly to how we can get an action’s pending state through useActionState
, we can use useFormStatus
to get a form action’s pending state from a component that’s included in a form.
For example a submit button component. It’s common to be its own component, shared across different forms across the app, so this hook makes it easy to access its containing form’s pending state:
"use client"
import { useActionState } from "react"
import { myServerAction } from './actions'
const initialState = {
message: "",
}
export const Demo = () => {
const [state, formAction, pending] =
useActionState(myServerAction, initialState)
return (
<div>
<form action={formAction}>
<input
type='text'
name='fullName'
/>
{state?.message && <p>{state.message}</p>}
<SubmitButton />
</form>
</div>
)
}
const SubmitButton = () => {
const { pending } = useFormStatus()
return (
<button
aria-disabled={pending}
type='submit'>
{pending ? "Submitting..." : "Submit"}
</button>
)
}
The useOptimistic
hook
This hook lets you update the UI immediately in response to an action, before the server responds.
You pass to it the current state you want to manage through it (for example, an array messages
), and a function to update the optimistic state.
It returns you the optimistic state (which you use for immediate rendering), and a function to update it.
You call the hook before the server request.
After the server response is received, you update the actual state (in the example, messages
, and in the TSX you render the optimistic state, which after you update with the actual state, renders the actual state.
You can use this hook in a client component.
Here’s a simple todo app example.
Define an addTodo
server action:
'use server'
type Todo = {
todo: string
}
export async function addTodo(newTodo: string): Promise<Todo> {
// Simulating server delay
await new Promise((resolve) => setTimeout(resolve, 3000))
// Add todo on server
return {
todo: newTodo + ' test',
}
}
In this server action, after 3 seconds we return the todo sent, plus ' test'
to understand that this is the returned value from the server action.
Client-side we use the useOptimistic
hook to generate an optimisticTodos
array, which we use to list todos in the TSX:
const [optimisticTodos, addOptimisticTodo] = useOptimistic<Todo[], string>(
todos,
(state, newTodo) => [...state, { todo: newTodo }]
)
When we hit the form action (the user presses the submit button), the addOptimisticTodo()
function is called to add the new todo.
Then we hit the server, which takes some time as the server action waits 3 seconds).
Finally when we’re back we call setTodos()
to update the todos list with the actual state coming from the server.
Full code:
'use client'
import { useOptimistic, useState, useRef } from 'react'
import { addTodo } from './actions'
type Todo = {
todo: string
}
export default function Todos() {
const [todos, setTodos] = useState<Todo[]>([])
const formRef = useRef<HTMLFormElement>(null)
const [optimisticTodos, addOptimisticTodo] = useOptimistic<Todo[], string>(
todos,
(state, newTodo) => [...state, { todo: newTodo }]
)
const formAction = async (formData: FormData) => {
const todo = formData.get('todo') as string
addOptimisticTodo(todo)
formRef.current?.reset()
try {
const result = await addTodo(todo)
// Update the actual state with the server response
setTodos((prevTodos) => [...prevTodos, { todo: result.todo }])
} catch (error) {
console.error('Error adding todo:', error)
// Optionally, you could remove the optimistic update here if the server request fails
}
}
return (
<div>
{optimisticTodos.map((m, i) => (
<div key={i}>{m.todo}</div>
))}
<form action={formAction} ref={formRef}>
<input type='text' name='todo' />
<button type='submit'>Send</button>
</form>
</div>
)
}
The use
hook
When you use Suspense, you can use the use()
hook, and pass it a promise (or other values) and React will suspend that component until the promise resolves:
<Suspense fallback={<Spinner />}>
<Profile userId={123} />
</Suspense>
import { use } from 'react'
async function fetchUser() {
//....
}
export function Profile({ userId }) {
const user = use(fetchUser(userId));
return <h1>{user.name}</h1>;
}
This is a “special” hook because hooks normally must be called at the top of a component, but use()
does not have this limitation.
This new function simplifies creating smooth data loading experiences.
More
There are more features I would consider minor, and the above are the most important / impactful.
Soon I will update my React Handbook and my Next.js Handbook to cover the latest versions of both.
Make sure you join my newsletter to be notified when I release them.
→ I wrote 17 books to help you become a better developer, download them all at $0 cost by joining my newsletter
→ JOIN MY CODING BOOTCAMP, an amazing cohort course that will be a huge step up in your coding career - covering React, Next.js - next edition February 2025