Log in to GraphQL EditorGet started
Unlocking the Power of React 19

Tomasz Gajda

10/31/2024

Unlocking the Power of React 19

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.

1. React Compiler

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.

Enhanced Performance

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.

Simplicity and Readability

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.

2. Server Components

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.

Benefits of Server Components

  1. Improved Performance: By reducing the amount of JavaScript needed on the client side, Server Components can lead to smaller bundle sizes and faster initial renders. Since the server handles the heavy lifting of rendering, clients can focus on interactive elements without the overhead of managing complex states.
  2. Easier Data Fetching: Server Components simplify data fetching by allowing developers to fetch data directly within the component logic. This integration eliminates the need for additional libraries like Axios or Fetch for API calls, as data can be fetched as part of the rendering process​. This approach not only reduces boilerplate code but also enhances the overall developer experience.
  3. Seamless Integration: Server Components work harmoniously with traditional client components. This hybrid model allows developers to choose where and how to render different parts of their application, optimising both server and client resources​. This flexibility is especially beneficial for applications with varying content needs, allowing developers to mix static and dynamic content effortlessly.
  4. Better SEO: Since Server Components render HTML on the server, they improve search engine optimization (SEO). Search engines can crawl the pre-rendered HTML, making it easier for them to index the content. This is crucial for applications that rely on organic search traffic for visibility.

3. Directives

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.

The use client Directive

Since 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.

The use server Directive

While 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.

4. Actions

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.

What Are Actions?

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.

Server Actions

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​.

Why Use Actions?

With Actions, React 19 simplifies data handling by enabling centralised functions that are accessible across the client-server boundary. Key advantages include:

  1. Cleaner Code: Actions remove the need for custom event handlers in many cases, making code more concise and readable.
  2. Improved Server Interactions: Server Actions allow direct access to server-side resources, reducing the need for API calls and improving performance.
  3. Enhanced Developer Experience: Actions streamline handling of async data and transitions, integrating smoothly with concurrent features for better UI responsiveness.

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.

5. New Hooks

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 Submission

The 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 Status

The 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 Experience

The 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.

5. The new use API

React 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.

Handling Promises Directly with 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​.

Grouped Rendering with Suspense Boundaries

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.

6. Preloading Resources

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.

Key Preloading APIs and Their Uses

  1. 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.
  2. 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.
  3. 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.
  4. preloadModule: Prepares an ESM (ECMAScript Module) that the application plans to use, reducing the wait time when the module is later imported.
  5. preinit: Fetches and evaluates an external script or fetches a stylesheet, making it ready before rendering the relevant components.
  6. 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>

Leveraging Frameworks for Automatic Preloading

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.

Summary

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.

Check out our other blogposts

GraphQL cache: using LRU cache with GraphQL Zeus
Michał Tyszkiewicz
Michał Tyszkiewicz
GraphQL cache: using LRU cache with GraphQL Zeus
1 min read
17 days ago
Unlocking the Power of React 19
Tomasz Gajda
Tomasz Gajda
Unlocking the Power of React 19
1 min read
2 months ago
Zeus update - GraphQL spread operator
Michał Tyszkiewicz
Michał Tyszkiewicz
Zeus update - GraphQL spread operator
1 min read
3 months ago

Ready for take-off?

Elevate your work with our editor that combines world-class visual graph, documentation and API console

Get Started with GraphQL Editor