SCROLL TO EXPLORE •SCROLL TO EXPLORE •
A first look at <mark>React 19</mark>

A first look at React 19

Let's take a look at the improvements and new parts of React 19

Eddy Vinck - 6 min read

The beta release of React 19 has just been released. There are some great quality of life improvements and some new things.

The quality of life improvements in React 19

There are quite a few improvements to the existing parts of React:

  • You no longer need forwardRef
  • Put document metadata anywhere in your JSX
  • Preloading resources to improve performance
  • Better error messages

Goodbye forwardRef

A ref can now be passed as a prop. This was possible before using a prop name other than ref such as buttonRef, but now you can access the ref as a prop in your component without forwardRef:

function MyInput({placeholder, ref}) {
  return <input placeholder={placeholder} ref={ref} />
}

Metadata in JSX

React 19 has official support for metadata inside components. Before React 19 you would need a package like react-helmet-async to achieve this.

With the example below, the meta title and other related items will be updated inside the <head> tag of your page.

function BlogPost({post}) {
  return (
    <article>
      <h1>{post.title}</h1>
      <title>{post.title}</title>
      <meta name="author" content="Josh" />
      <link rel="author" href="https://twitter.com/joshcstory/" />
      <meta name="keywords" content={post.keywords} />
      <p>
        Eee equals em-see-squared...
      </p>
    </article>
  );
}

Stylesheets are supported too:

function Component() {
  return (
    <Suspense fallback="loading...">
      <link rel="stylesheet" href="foo" precedence="default" />
      <article class="foo-class bar-class">
        {...}
      </article>
    </Suspense>
  )
}

Better error messages

Errors were previously thrown twice by an Error Boundary, and then logged using console.error to show you more information about the error. In React 19, you will see a single error with all the information you need. This is a very welcome change as it declutters your debugging console.

The new parts of React 19

  • Asyncronous functions in transitions
  • A new useActionState hook
  • Form actions in react-dom and useFormStatus
  • A new useOptimistic hook
  • new use API
  • Custom element support (web components)

Asyncronous functions in transitions

Traditionally you could already use useState or alternative state management solutions to manage all the various data, loading, and error states in your forms.

React 18 released the useTransition hook. This hook lets you update state without blocking the UI. This helps keep the UI updates responsive on slower end-user devices like older phones, and was a useful addition to managing state.

The useTransition hook can now start a transition with an async function. In the following example, React will manage the state of isPending for you, which can save you some boilerplate logic you would previously have to write using useState. While your async function is running isPending will be true. After the async function succeeds isPending will automatically be set to false.

function UpdateName() {
  const [name, setName] = useState("");
  const [error, setError] = useState(null);
  const [isPending, startTransition] = useTransition();

  const handleSubmit = () => {
    startTransition(async () => {
      const error = await updateName(name);
      if (error) {
        setError(error);
        return;
      } 
      redirect("/path");
    })
  };

  return (
    <div>
      <input value={name} onChange={(event) => setName(event.target.value)} />
      <button onClick={handleSubmit} disabled={isPending}>
        Update
      </button>
      {error && <p>{error}</p>}
    </div>
  );
}

The new useActionState hook

Asyncronous transitions are called "actions" by convention in React. A new useActionState hook was introduced that helps handle the most common use-cases for actions.

The useActionState can handle error and isPending states. If you've used TanStack Query or Redux Toolkit Query before, you could see this as a simpler alternative for when you don't need the additional features from those libraries. Note that these sorts of libraries still provide many additional options that could benefit your user's experience.

function ChangeName({ name, setName }) {
  const [error, submitAction, isPending] = useActionState(
    async (previousState, formData) => {
      const error = await updateName(formData.get("name"));
      if (error) {
        return error;
      }
      redirect("/path");
      return null;
    },
    null,
  );

  return (
    <form action={submitAction}>
      <input type="text" name="name" />
      <button type="submit" disabled={isPending}>Update</button>
      {error && <p>{error}</p>}
    </form>
  );
}

Form Actions in react-dom

Updates have been made to react-dom that make implementing these actions even easier.

You can now pass an async function to a form's action attribute:

<form action={actionFunction}>

When the actionFunction returns successfully, the form will be reset automatically for uncontrolled components (components where you don't manage the state via useState or other state management options).

You can also add the action to an <input> or a <button>.

A new hook useFormStatus is now also available, which lets you check the form status in child components in a form:

import {useFormStatus} from 'react-dom';

function DesignButton() {
  const {pending} = useFormStatus();
  return <button type="submit" disabled={pending} />
}

I've used this hook before in a test application built with Next.js when the hook was still experimental, so it's good to see this hook available without the experimental warning.

Here is a snippet from an UploadButton component I made which utilizes server actions in Next.js:

"use client";

import { experimental_useFormStatus as useFormStatus } from "react-dom";

export function SubmitButton() {
  const { pending } = useFormStatus();

  return (
    <button className="bg-slate-600 p-2 px-6 rounded-md" disabled={pending}>
      {pending ? "Submitting..." : "Submit"}
    </button>
  );
}

What I like about this, is that it removes the need for a lot of boilerplate code originally required to manage your form state. Passing props or providing a custom React Context is not necessary in a lot of cases now, which is great!

The new useOptimistic hook

There is a new hook that helps with managing optimistic UI. The Remix documentation has a good defintion for optimistic UI if you're unsure what it means:

Optimistic UI: Optimistic UI enhances perceived speed and responsiveness by immediately updating the UI with an expected state before the server's response is received

Remix documentation (link)

In the following example, the useOptimistic will render the submitted name immediately when the form submits. When the submit action finishes or errors, React will then continue to use the currentName again.

function ChangeName({currentName, onUpdateName}) {
  const [optimisticName, setOptimisticName] = useOptimistic(currentName);

  const submitAction = async (formData) => {
    const newName = formData.get("name");
    setOptimisticName(newName);
    const updatedName = await updateName(newName);
    onUpdateName(updatedName);
  };

  return (
    <form action={submitAction}>
      <p>Your name is: {optimisticName}</p>
      <p>
        <label>Change Name:</label>
        <input
          type="text"
          name="name"
          disabled={currentName !== optimisticName}
        />
      </p>
    </form>
  );
}

The new multifunctional use API

React has introduced a new flexible API with multiple use-cases. It's just called use, and according to the official React 19 beta announcement article you can use it to "read resources in render." Practically, it means you can use it for reading promises (which is useful when you're using Suspense) and reading a React Context.

import {use} from 'react';
import ThemeContext from './ThemeContext'

function Comments({commentsPromise}) {
  // `use` will suspend until the promise resolves.
  const comments = use(commentsPromise);
  const theme = use(ThemeContext);

  return comments.map(comment => <p style={{color: theme.color}} key={comment.id}>{comment}</p>);
}

function Page({commentsPromise}) {
  // When `use` suspends in Comments,
  // this Suspense boundary will be shown.
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Comments commentsPromise={commentsPromise} />
    </Suspense>
  )
}

See the official use documentation page for more information.

Support for web components in React

Web components (or Custom Elements) are now fully supported in React. Previously there were some issues when using custom elements in React, but now they should behave as you would expect when doing Server Side Rendering or Client Side Rendering.

Should you upgrade to React 19?

React 19 is in beta right now. The React team recommends waiting until frameworks have implemented it under the hood before you upgrade to React 19 for your application.

This release is mostly to prepare library authors, so that all your favorite React libraries will be ready when React 19 goes out of beta. There are great improvements and features in this new React version, but we will have to wait until our favorite React frameworks have implemented them.

For the full release notes, see the full React 19 beta release notes on react.dev. If you're curious what changes you need to make when upgrading, be sure to take a look at the official React 19 upgrade guide.

Photo of Eddy Vinck

Eddy Vinck

Senior Software Engineer. Conference speaker.