React 19 is a big step forward for the React ecosystem, packed with new features and improvements to make the development experience better and our apps more powerful. Let’s take a look at the new hooks and APIs, React Server Components and more.
React 19, which went stable December 5, 2024, debuts two years after the release of React 18, marking a significant milestone in its evolution. This new version is filled with numerous updates, such as new hooks and APIs, React Server Components, the removal of some outdated React APIs, and much more. In today’s article, we’ll discuss some of these new features and how they can enhance your React development experience.
React v19 is official! Take the latest in React for a spin with KendoReact Day-Zero support for React 19. Just update to the latest version.
React 19 introduces a variety of new hooks and APIs designed to streamline and enhance the development process. These additions provide more powerful and flexible tools for managing state, handling asynchronous operations and optimizing performance in our React applications. Let’s dive into some of the most of these new Hooks and APIs.
A significant aspect of using web applications today is interacting with forms for a wide range of purposes, such as user authentication, data submission, ecommerce transactions, feedback collection, search queries and more. Consequently, a common task in React component development involves handling form submissions and managing the asynchronous updates that follow these submissions.
A simple example of a form component handling such async updates can look something like the following:
import React, { useState, useCallback } from "react";
const submitForm = async () => {
/* form submit */
};
export function Component() {
// create form state
const [formState, setFormState] = useState(null);
const [isPending, setIsPending] = useState(false);
// handle form submission
const formAction = useCallback(async (event) => {
event.preventDefault();
setIsPending(true);
try {
const result = await submitForm();
setFormState(result);
} catch (error) {
setFormState({ message: "Failed to complete action" });
}
setIsPending(false);
}, []);
// display form template
return <form onSubmit={formAction}>{/* Form Template */}</form>;
}
In the example above, the component handles the form’s state and processes form submissions asynchronously. When the form is submitted, the submitForm()
function sends the form data to a server and receives a response. The component then updates its state to provide feedback to the user about the submission process.
In React 18, the concept of transitioning the UI from one view to another in a non-urgent manner was referred to as transitions. React 19 extends this functionality by supporting the use of async functions in transitions. The useTransition
hook can now be utilized to manage the display of loading indicators or placeholders during asynchronous data fetching.
In React 19, the concept of transitions is taken a step further as functions that use async transitions are now referred to as Actions. There now exist a few specialized hooks to manage Actions like what we’ve seen above and the first we’ll take a look at is the useActionState hook.
The useActionState()
hook takes three parameters:
It returns three values in a tuple:
import { useActionState } from "react";
export function Component() {
const [state, dispatch, isPending] = useActionState(
action,
initialState,
permalink
);
// ...
}
The action function, which is the first argument passed to the useActionState
hook, is invoked when the form is submitted. It returns the form state we expect to transition to, whether the submission is successful or encounters errors. This function receives two parameters: the current state of the form and the form data at the moment the action was triggered.
Here’s an example of creating an action()
function that calls a hypothetical submitForm()
function, which then triggers an API call to send the form data to a server. If the action is successful, it returns a form state object representing the updated state of the form. If the action fails, it returns a form state object reflecting the error state, potentially including error messages or indicators to help the user correct the issue.
import { useActionState } from "react";
const submitForm = async (formData) => {
/* form submit fn that calls API */
};
const action = async (currentState, formData) => {
try {
const result = await submitForm(formData);
return { message: result };
} catch {
return { message: "Failed to complete action" };
}
};
export function Component() {
const [state, dispatch, isPending] = useActionState(action, null);
// ...
}
With our useActionState()
hook set up, we can now utilize the form state, dispatch()
function and isPending
values in our form template.
With React 19, <form>
elements now have an action
prop that can receive an action function that will be triggered when a form is submitted. Here is where we’ll pass down the dispatch
function from our useActionState()
hook.
import { useActionState } from "react";
const submitForm = async (formData) => {
/* form submit fn that calls API */
};
const action = async (currentState, formData) => {
try {
const result = await submitForm(formData);
return { message: result };
} catch {
return { message: "Failed to complete action" };
}
};
export function Component() {
const [state, dispatch, isPending] = useActionState(action, null);
return <form action={dispatch}>{/* ... */}</form>;
}
We can show the form state
within our template and use the isPending
value to indicate to the user when the asynchronous action is in progress.
import { useActionState } from "react";
const submitForm = async (formData) => {
/* form submit fn that calls API */
};
const action = async (currentState, formData) => {
try {
const result = await submitForm(formData);
return { message: result };
} catch {
return { message: "Failed to complete action" };
}
};
export function Component() {
const [state, dispatch, isPending] = useActionState(action, null);
return (
<form action={dispatch}>
<input type="text" name="text" disabled={isPending} />
<button type="submit" disabled={isPending}>
Add Todo
</button>
{/*
display form state message to convey when
form submit is successful or fails.
*/}
{state.message && <h4>{state.message}</h4>}
</form>
);
}
Thanks to these new changes in React, there’s no longer a need to manually handle pending states, errors and sequential requests when working with async transitions in forms. Instead, we can access these values directly through the useActionState()
hook.
For a more detailed example that interacts with the publicly available dummyjson API, check out this GitHub Gist.
React 19 introduces a new hook called useFormStatus, which enables nested child components to access information about the form they are part of, similar to how a form would act as a context provider.
import { useFormStatus } from "react";
export function NestedComponent() {
/* access form information */
const { pending, data, method, action } = useFormStatus();
return (
/* template */
);
}
Though accessing parent form information can be achieved using Context, React 19 introduces the useFormStatus
hook to simplify handling form data within nested components.
Another new addition in React 19 is the useOptimistic hook. It enables optimistic updates while a background operation, such as a network request, is ongoing. This enhances the user experience by providing quicker feedback on interactions.
Here’s an example of how the useOptimistic()
hook can be used to manage optimistic updates for a message
state property passed as props.
import { useOptimistic } from "react";
export function Component({ message, updateMessage }) {
// create "optimistic" state property
const [optimisticMessage, setOptimisticMessage] = useOptimistic(message);
const submitForm = async (formData) => {
const newMessage = formData.get("text");
// before triggering API change, set new value optimistically
setOptimisticMessage(newMessage);
// update actual state when API submission resolves
const updatedName = await submitToAPI(newMessage);
updateMessage(updatedName);
};
return (
<form action={submitForm}>
{/* show optimistic value */}
<p>{optimisticMessage}</p>
<input type="text" name="text" />
<button type="submit">Add Message</button>
</form>
);
}
In the above component example, the useOptimistic
hook manages optimistic updates for the message
state passed down as a prop.
When the user submits the form by clicking the “Add Message” button, the submitForm()
function is triggered. Before making the API request to update the message, the setOptimisticMessage()
function is called with the new message value from the form data. This instantly updates the UI to reflect the optimistic change, providing immediate feedback to the user.
Once the update completes or fails, React will automatically revert to the message
prop value.
React 19 introduces a new use API, providing a flexible method to read values from resources such as promises or context. For example, to access context values, we pass the context to use()
, and it navigates the component tree to find the nearest context provider.
import { use, createContext } from "react";
const Context = createContext({ data: "Data from context" });
// nested component
function NestedChildComponent() {
const context = use(Context);
// ...
}
Unlike the useContext()
hook, which is traditionally used to read context, the use()
function can be utilized within conditionals and loops in our components!
import { use, createContext } from "react";
const Context = createContext({ data: "Data from context" });
// nested component
function NestedChildComponent({ value }) {
if (value) {
const context = use(Context);
}
// ...
}
The use()
function also integrates seamlessly with Suspense and error boundaries to read promises (for more details, see the “Streaming data from the server to the client” section of the React documentation).
React Server Components, a new feature in React 19, enables the creation of stateless React components that run on the server. These components execute ahead of time and prior to bundling in an environment separate from the client application or server-side-rendered server.
Because React Server Components run on a web server, they can access the data layer directly without needing to interact with an API!
import db from "./database";
// React Server Component
async function BlogPost({ postId }) {
// Load blog post data from database
const post = await db.posts.get(postId);
// Load comments for the post from database
const comments = await db.comments.getByPostId(postId);
return (
<div>
<h2>{post.title}</h2>
<p>{post.content}</p>
<h3>Comments</h3>
<ul>
{comments.map((comment) => (
<li key={comment.id}>
<Comment {...comment} />
</li>
))}
</ul>
</div>
);
}
How cool is that! This eliminates the need to expose an API endpoint or use additional client-side fetching logic to load data into our components since all data handling is performed on the server.
Remember, Server Components run on the server, not the browser, so they cannot use traditional React component APIs like useState
. To add interactivity to a React Server Component, we need to use Client Components that complement Server Components for handling interactivity.
Continuing the above blog post example, this would involve rendering the Comment
component as a Client component that includes some state and interactivity.
// React Client Component
"use client";
export function Comment({ id, text }) {
const [likes, setLikes] = useState(0);
function handleLike() {
setLikes(likes + 1);
}
return (
<div>
<p>{text}</p>
<button onClick={handleLike}>Like ({likes})</button>
</div>
);
}
Note the "use client"
declaration at the top of the component file in the example above. This indicates that the component is a Client Component when working with React Server Components. This means it can manage state, handle user interactions and utilize browser-specific APIs. This directive informs the React framework and bundler to treat this component differently from Server Components, which are stateless and run on the server.
On the flip side, React Server Components are the default, so we don’t need to state “use server” at the top of Server Component files. Instead, “use server” is only used to designate server-side functions that can be called from Client Components. These functions are known as Server Actions.
Though written while React 19 was still in beta, Jonathan Gamble has written a great, concise post on The React Server Components Paradigm.
React 19 also introduces several other enhancements to further improve developer experience and application performance.
In React 19, we can now pass ref
directly as a prop to function components:
function MyInput({ placeholder, ref }) {
return <input placeholder={placeholder} ref={ref} />;
}
// passing ref down as prop
<MyInput ref={ref} />;
This change means we no longer require forwardRef, which will be deprecated and removed in future React versions.
With React 19, we’ll be able to render <Context>
directly as a provider instead of having to specify <Context.Provider>
, simplifying the syntax:
import { createContext } from "react";
const MyContext = createContext();
function App() {
return (
<MyContext value={/* some value */}>
{/* Your components */}
</MyContext>
);
}
We can now return a cleanup function from ref callbacks in React 19:
<input
ref={(ref) => {
// Ref created
return () => {
// Ref cleanup
};
}}
/>
Just like how the cleanup function behaves in the useEffect
hook, the cleanup function returned from the ref callback will be called when the component is unmounted.
React 19 adds native support for rendering document metadata tags such as <title>
, <link>
and <meta>
within components, whereas in the past where in the past developers typically relied on external libraries like react-helmet or next/head for managing these tags.
function Component({ data }) {
return (
<article>
<title>{post.title}</title>
<meta name="author" content="Hassan Djirdeh" />
<link rel="author" href="https://twitter.com/djirdehh/" />
<meta name="keywords" content={post.keywords} />
</article>
);
}
These tags are automatically hoisted to the <head>
section of the document, ensuring compatibility with client-only apps, streaming SSR and Server Components.
Error reporting for hydration errors in react-dom
has been improved. Instead of logging multiple errors in development mode without detailed information, React 19 now logs a single message with a diff of the mismatch.
Uncaught Error: Hydration failed because the server rendered HTML didn’t match the client. As a result this tree will be regenerated on the client. This can happen if an SSR-ed Client Component is used:
- A server/client branch if (typeof window !== 'undefined').
- Variable input such as Date.now() or Math.random() which changes each time it’s called.
- Date formatting in a user’s locale which doesn’t match the server.
- External changing data without sending a snapshot of it along with the HTML.
- Invalid HTML tag nesting.
It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.
https://react.dev/link/hydration-mismatch
<App>
<span>
+ Client
- Server
at throwOnHydrationMismatch
…
This clearer error message helps developers quickly identify and fix hydration issues.
Similarly, in React 19, improvements have been made to error reporting for handling caught and uncaught errors, logging a single error message with all relevant information instead of duplicate error messages. Additionally, two new root options have been added for handling different types of errors to complement the existing onRecoverableError
root option:
onCaughtError
: called when React catches an error in an Error BoundaryonUncaughtError
: called when an error is thrown and not caught by an Error BoundaryReact 19 also includes:
You can find more details and information in the Improvements in React 19 section of the React documentation.
React 19 is a big step forward for the React ecosystem, packed with new features and improvements to make the development experience better and our apps more powerful. With new hooks and APIs, React Server Components and the removal of outdated APIs, there’s a lot to get excited about.
The updates introduced in React 19 bring some breaking changes, but the React team has taken steps to ensure the upgrade process is as smooth as possible. You can find more details on how best to upgrade in the React 19 Upgrade Guide from the official React documentation.
Hassan is a senior frontend engineer and has helped build large production applications at-scale at organizations like Doordash, Instacart and Shopify. Hassan is also a published author and course instructor where he’s helped thousands of students learn in-depth frontend engineering skills like React, Vue, TypeScript, and GraphQL.