Tomasz Gajda
10/31/2024
With the beta release of React 19, the JavaScript library continues to evolve, pushing the boundaries of web development. As developers navigate the complexities of building modern applications, React 19 introduces a range of enhancements aimed at simplifying workflows and boosting performance. This update reflects community feedback and the ever-changing technology landscape, reinforcing React's position as a leading choice for developers worldwide.
In this blog post, we will explore the significant changes that React 19 brings to the table, examining how these innovations can transform your approach to building applications. Whether you're a seasoned developer or just starting out, understanding these updates will be crucial for leveraging React's full potential in your projects as we await its official production release.
One of the standout features of React 19 is the introduction of React Compiler, fundamentally changing how applications are built and optimised. This compiler offers numerous advantages over previous implementations, particularly regarding performance and integration with modern build tools.
The new compiler processes components at build time rather than runtime, which significantly reduces the workload on the browser and leads to faster application performance. This transition allows developers to deliver content more quickly, enhancing user engagement and overall experience.
In the past, developers primarily relied on bundlers to manage the optimization process, performing various transformations and optimizations during the build phase. However, this often required additional performance hooks to manage state and lifecycle events effectively. With the new compiler, many of these performance hooks may no longer be necessary, as the compiler streamlines and optimises the code behind the scenes. This shift simplifies the development process and allows for cleaner, more efficient code.
Beyond performance enhancements, the new compiler promotes better code organisation and readability. By transforming complex components into more manageable structures, developers maintain cleaner codebases. This simplification fosters improved collaboration among teams and makes it easier to onboard new developers.
Moreover, the compiler enhances TypeScript integration, providing better type inference and error detection, which helps catch potential issues during development rather than at runtime.
Server Components allow developers to offload some of the rendering processes from the client to the server. Unlike traditional client-side components, which rely on the browser to render and manage application state, Server Components are rendered on the server and sent as HTML to the client. This shift means that users can receive pre-rendered content faster, leading to quicker load times and improved performance.
React 19 enhances client-server communication through the introduction of new directives
that clarify where specific pieces of code should execute. While these directives are not unique to React 19, they are integral to the functionality of React Server Components. As RSC becomes the default configuration, these directives enable developers to clearly differentiate between client-side and server-side code, ensuring that the appropriate logic runs in the intended environment. This distinction is crucial for bundlers, which need to know how to handle various components and functions effectively.
use client
DirectiveSince Server Components are now the default in React, all components initially run on the server unless otherwise specified. The use client
directive helps distinguish components that require client-side functionality, such as user interactions and state management through hooks. By adding use client
at the top of a file, developers mark that component to run exclusively on the client. This change allows React to automatically optimise components, only bundling and shipping the code when necessary for client-side interaction.
For example, components that manage local state with useState
, handle DOM events, or access the browser environment need to be marked with use client
. This directive thus provides clarity while keeping client bundles leaner by excluding unnecessary server-side logic.
use server
DirectiveWhile components default to server-side execution, the use server
directive plays a specialised role by marking Server Actions—functions that can be executed server-side while being callable from client-side components. However, not every server-rendered component needs this label; use server
applies specifically to functions within components, ensuring these actions are exclusively run on the server. For added control, developers can use the server-only npm package, which restricts specific code from running on the client side, enforcing security and performance optimizations.
An example of use server
is a function that interacts with a database or performs heavy computational tasks that the client doesn’t need to handle. By marking these functions with use server
, developers can ensure sensitive data and processes stay on the server, reducing potential security vulnerabilities while optimising client load.
One of the key advancements in React 19 is the introduction of Actions, a feature designed to replace traditional event handlers while integrating seamlessly with React's transitions and concurrent rendering capabilities. Actions provide a new way to handle asynchronous data flow and simplify component interaction by centralising logic that previously relied on individual event handling.
Actions in React allow developers to define functions that can be called directly from components without the need for separate event handlers. Unlike previous event handlers—like onSubmit
for form submissions—Actions can be passed directly to components, where they handle both client-side and server-side logic. This flexibility makes it easier to manage data flow between client and server, especially in complex applications.
For example, consider a formAction
defined within a TodoApp
component. This function can receive FormData
directly, eliminating the need to manually parse events before updating the state or making server requests:
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");
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>
</>
);
}
In this example, formAction
is a Client Action that manages form submission, enabling developers to handle form data more intuitively. Rather than needing to set up individual onChange
or onSubmit
handlers, developers can manage data submission directly through the formAction
.
Taking it a step further, Server Actions allow developers to define functions that can be executed on the server, while still being callable from Client Components. This approach eliminates the need to set up custom API endpoints for routine server interactions, like saving data to a database, as the action can directly interact with server-side resources.
For instance, imagine creating a new server-side action to handle form submissions in a TodoList
component. Using use server
, this action can be executed server-side to perform tasks like database insertion:
// actions.ts
'use server'
export async function create() {
// Insert data into database
}
In the component, the create
Server Action can be connected directly to the form, bypassing the need for intermediate API routes:
// 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>
</>
);
}
In this setup, create
is a Server Action that enables the client to trigger server-side behaviour, such as writing data to a database, in response to user actions. By integrating Server Actions, developers can improve application performance and security, as these operations only execute server-side without exposing sensitive data to the client.
With Actions, React 19 simplifies data handling by enabling centralised functions that are accessible across the client-server boundary. Key advantages include:
By integrating Actions, React 19 empowers developers to build more interactive and responsive applications with minimal boilerplate, delivering an improved experience for both developers and users.
React 19 introduces a set of hooks designed to make handling state and providing feedback more efficient. These hooks—useActionState
, useFormStatus
, and useOptimistic
—are particularly valuable for handling forms and user interactions but are versatile enough to apply to other UI elements. They complement Actions by streamlining asynchronous data handling, reducing boilerplate, and improving the user experience with real-time feedback.
useActionState
: Streamlining Form State and SubmissionThe useActionState
hook simplifies managing form data and validation, particularly when using Actions to handle form submissions. By encapsulating common form-related state (such as input values, errors, and pending state) within a single hook, useActionState
reduces the need for custom state management logic. It even provides a pending
state that can be used to display a loading indicator during form submission, enhancing the user experience.
For example, in a signup form, useActionState
can manage the initial form state, handle data input, and update the UI based on form validation or errors, as shown below:
"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>
);
}
In this setup, useActionState
handles the form state and displays a submission message while pending
is true, creating a smooth, responsive form experience without requiring additional state-handling logic.
useFormStatus
: Monitoring Form Submission StatusThe useFormStatus
hook provides insight into the status of the latest form submission, specifically whether the form submission is pending. This hook is particularly useful in shared or reusable components or scenarios where there are multiple forms on the same page. It ensures that only the status of the parent form is returned, enabling more focused control over form elements.
In the example below, useFormStatus
checks whether a form submission is in progress, disabling the submit button accordingly:
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>
);
}
While useActionState
includes a pending
state, useFormStatus
shines when form state isn’t needed, such as in shared components or multiple form pages.
useOptimistic
: Real-Time UI Updates for a Smoother ExperienceThe useOptimistic
hook introduces optimistic UI updates by allowing temporary state changes in the UI before the server confirms them. This approach is useful for interactions that should appear instantaneously to the user—such as adding an item to a list—without waiting for a server response. When the server completes the request, the UI can then update with the final state, ensuring consistency.
In the example below, a new message is optimistically added to a thread, showing up immediately in the UI while the action is processed on the server:
"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");
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>
);
}
In this example, useOptimistic
allows the user to see their new message immediately in the thread while it is still being sent to the server, resulting in a more fluid and responsive interaction.
use
APIReact 19 introduces the use
function, a new API that enhances how promises and contexts are managed within component rendering. Unlike conventional hooks, use can be called in loops, conditionals, and early returns, allowing more flexible integration into component logic. This feature brings significant improvements in handling async data, loading states, and error handling in a clean, declarative way.
use
The use
API makes working with promises simpler by allowing React to directly handle async values. When a promise is passed to use
, it pauses rendering until the promise resolves. This approach minimises the need for extra state management, boilerplate code, and nested callbacks in async operations. With use
, the nearest Suspense boundary takes charge of loading states and error handling, ensuring smooth transitions while async data loads.
Consider this example where use
waits for a cartPromise
to resolve before rendering the cart items:
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 (
<Suspense fallback={<div>Loading...</div>}>
<Cart cartPromise={cartPromise} />
</Suspense>
);
}
In this setup, when use
encounters a promise, it suspends the Cart
component’s rendering until the data is available. During this time, the <Suspense>
boundary displays a fallback (in this case, "Loading..."), creating a smoother experience for users without needing explicit state or loading indicators in each component.
The use
function can also be used to group multiple components together under a single Suspense boundary, making it possible to render all components only when all required data is available. This reduces flickering, ensures more cohesive content loading, and enhances performance, especially when dealing with multiple async requests that should render simultaneously.
With use
and Suspense boundaries, React 19 takes async rendering to a new level, providing developers with a tool to manage complex data loading patterns while keeping components simple and readable. By eliminating the need for traditional loading indicators and boilerplate, use
helps create a more natural user experience for applications with async data requirements.
React 19 introduces new APIs for preloading and prefetching resources, focusing on improving page load times and creating a smoother user experience. These APIs allow developers to anticipate resource needs, such as scripts, stylesheets, fonts, and server connections, which results in faster, more efficient loading by handling resource preparation ahead of time.
prefetchDNS
: This function prefetches the DNS of an external domain you anticipate connecting to, reducing the initial connection time for resources loaded from that domain.preconnect
: Initiates a connection to an external server in advance, even if the specific resources you’ll load aren’t known yet. This is helpful when you’re sure of a future connection but not the exact resources needed.preload
: Loads a specific resource (e.g., stylesheet, font, image, or script) you expect to use soon. By preloading these, React ensures they’re ready as soon as they’re needed.preloadModule
: Prepares an ESM (ECMAScript Module) that the application plans to use, reducing the wait time when the module is later imported.preinit
: Fetches and evaluates an external script or fetches a stylesheet, making it ready before rendering the relevant components.preinitModule
: Similar to preinit
, but specifically for ESM modules.Here’s an example where these functions are called in a React component:
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://...");
}
This code prepares various resources, and React will output corresponding HTML tags in the <head>
section, like:
<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>
Many React frameworks automate preloading, which can relieve developers of manually calling these APIs. However, React’s new preloading APIs give developers more control over load prioritisation and connection timing. This leads to an overall improvement in load performance, especially on resource-intensive or multimedia-rich pages.
With these preloading tools, React 19 helps developers craft faster, more responsive applications by intelligently managing resource delivery in line with modern loading strategies.
The updates in React 19 introduce performance boosts and productivity tools, but they have also sparked some debate among developers. One of the most discussed aspects is the new use
API, which allows use
to be called within loops and conditionals, a departure from React's usual strict rules on hook usage. While this simplifies async data handling, some developers worry it could lead to less predictable behaviour in complex applications. Moreover, updates to hooks such as useActionState
and useOptimistic
have generated mixed reactions—on the one hand, they streamline actions and state management, but on the other, they introduce a learning curve for developers used to traditional hooks.
Server Components and preloading features aim to enhance rendering and load times, especially through server-side features and resource preloading. However, these may require adjustments in existing codebases, especially where SSR (server-side rendering) was implemented differently in older React versions. The simplification of certain features, like removing useMemo
and forwardRef
in some cases, has received both praise and criticism; while they improve code readability, they also signify another shift for developers to adapt to new patterns.
In sum, React 19's updates bring exciting potential for speed and efficiency but also entail a rethinking of standard practices that some developers have found challenging to integrate smoothly into existing workflows. This release is considered progressive but presents a controversial balance between ease-of-use improvements and changes to established best practices.