Back

What's New in React 19: Its 12 Latest Features and Updates

What's New in React 19: Its 12 Latest Features and Updates

React 19 is finally here! It came with some exciting updates, ranging from the React compiler to the introduction of new hooks, better error reporting, etc., and this article will explore a neat dozen of the latest features in React.

We’ll review a dozen of React 19’s new features. Let’s start with one that’s really different and then continue through the whole list.

The React Compiler

This tops the list as the latest addition to React. React 18 and below did not have a compiler; they instead used a transpiler. The major difference between these two is that the transpiler re-renders the whole component when state changes are made, and the compiler doesn’t. The compiler returns a memoized code. So, there is no more unnecessary re-rendering, and this would make React apps run faster.

Let’s run the code below in React 18 and React 19 and observe the differences in how the components re-render.

// Component to measure and log the time taken to render the header
function TimeToRender() {
  // Record the start time when the component begins rendering
  const startTime = performance.now();

  useEffect(() => {
    // Record the end time when the component has rendered
    const endTime = performance.now();
    // Log the time taken to render the component
    console.log(`CustomHeader render time: ${endTime - startTime}ms`);
 }, []); // Empty dependency array ensures this effect runs only once after the initial render

  return (
    <header>
      <h1>Counter App</h1>
    </header>
 );
}

// Main component of the Counter App
function CounterApp() {
  // State hook to manage the count value
  const [count, setCount] = useState(0);

  return (
    <>
      {/* Render the TimeToRender component */}
      <TimeToRender />
      <div>
        {/* Display the current count */}
        <p>{count}</p>
        {/* Button to increase the count */}
        <button onClick={() => setCount(count + 1)}>Increase</button>
        {/* Button to decrease the count */}
        <button onClick={() => setCount(count - 1)}>Decrease</button>
      </div>
    </>
 );
}

// Export the CounterApp component as the default export
export default CounterApp;

The code above is a counter app that increases and decreases the count when the button is pressed. It has a function called TimeToRender that measures the time it takes to render the component each time it is rendered.

The image below shows when the component renders in the compiler and the time it takes to re-render.

use (1)

The image below shows the time it takes for each re-render of the whole component in the transpiler.

compiler (1)

From the image above, we see that the transpiler re-renders multiple times, and the time for re-rendering decreases with each click, while the compiler only renders once.

Previously, we would have used memo, useMemo(), and useCallback() hooks to achieve this. This would make these hooks obsolete. In addition, we would have cleaner and fewer lines of code.

New Hooks

Hooks are one of React’s most popular features. They help manage state and lifecycle methods. React has built-in hooks and also offers the option of creating custom hooks.

In React 19, four new hooks were introduced:

The useTransition Hook

React 19 supports using async functions in transitions to manage state changes which may lead to UI changes. You can use the useTransition hook to update the status of a state to show if it is pending or not automatically.

This ensures that when a user triggers an update, it is handled smoothly. Also, the UI would reflect the correct state of the async function. Let’s look at a code example.

The code below is an app that adds an alphabet to a list. When the add button is clicked, it waits 4 seconds before adding the alphabet to the list. We can handle the pending state automatically using the startTransition async function.

import React, { useState, useTransition } from "react";

// Function to create a delay for a given number of milliseconds
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

// Asynchronous function to add an alphabet to the list after a delay
async function addAlphabetToList(alphabet) {
  await delay(4000);
  return null;
}

// Component to add an alphabet to a list
function AlphabetAdder() {
  const [alphabet, setAlphabet] = useState("");
  const [error, setError] = useState(null);
  // State and function for transition
  const [isPending, startTransition] = useTransition();
  const [alphabetList, setAlphabetList] = useState([]); // State to store the list of alphabets

  // Function to handle adding an alphabet to the list
  const handleAddAlphabet = async () => {
    startTransition(async () => {
      const error = await addAlphabetToList(alphabet); // Add alphabet to the list with delay
      if (error) {
        setError(error);
 } else {
        setAlphabetList([...alphabetList, alphabet]);
 }
 });
 };

  // Log the pending state
  console.log("Pending:", isPending);

  return <></>;
}

The image below shows that the pending state is true until the alphabet is added to the list after 4s.

usetransition

This contrasts with the previous versions where we handle the pending state manually using the setIsPendingding function to set the state to either true or false.

import React, { useState } from "react";

// Function to create a delay
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

// Async function to simulate adding an alphabet to a list after a delay
async function addAlphabetToList(alphabet) {
  await delay(4000);
  return null;
}

function AlphabetAdder() {
  const [alphabet, setAlphabet] = useState("");
  const [error, setError] = useState(null);
  // State to manage the pending state
  const [isPending, setIsPending] = useState(false);
  const [alphabetList, setAlphabetList] = useState([]);

  // Function to handle adding the alphabet
  const handleAddAlphabet = async () => {
    setIsPending(true);
    // Add the alphabet to the list after the delay
    const error = await addAlphabetToList(alphabet);
    if (error) {
      setError(error);
 } else {
      setAlphabetList([...alphabetList, alphabet]);
 }
    setIsPending(false);
 };

  // Log the pending state
  console.log("Pending:", isPending);

  return <></>;
}
export default AlphabetAdder;

The useActionState Hook

The useActionState hook is used to manage state changes in UI, just as the useTransition hook. What separates it from the useTransition hook above is that it assigns the error, action, and pending states in a single line of code instead of multiple lines, as seen above. Also, the useTransition hook only handles the pending state.

We can rewrite the code above using the useActionState, which gives us fewer lines of code.

const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function addAlphabetToList(name) {
  await delay(4000);
  return null;
}

function AlphabetAdder() {
  const [error, submitAction, isPending] = useActionState(
    async (previousState, formData) => {
      const error = await addAlphabetToList(formData.get("name"));
      if (error) {
        return error;
 }

      return null;
 },
    null,
 );

  console.log("Pending:", isPending);
  return <></>;
}

The code above uses the useActionState hook to automatically handle the error, submitAction, and isPending states without explicitly defining it using the useState hook or useTransition hook for pending states as we did earlier.

The useFormStatus Hook

The useFormStatus hook is used to access information about a submitted form. You can access if the form state is pending or not.

Here’s an example code that uses the useFormStatus hook to access the form’s pending state.

function FormStatusButton() {
  const { isPending } = useFormStatus();
  return (
    <div>
      <button type="submit" disabled={isPending}>
        {isPending ? "Adding..." : "Add"}
      </button>
    </div>
 );
}

Previously to access the pending state, we would have passed the state using a prop. In the code example below, we passed the isPending state as a prop to the CustomButton function to access it in the return statement.

function CustomButton({ isPending }) {
  return (
    <div>
      <button type="submit" disabled={isPending}>
        {isPending ? "Adding..." : "Add"}
      </button>
    </div>
 );
}

The useOptimistic Hook

This hook allows us to instantly update the UI when some state changes without waiting for an async action to be completed. For example, in chat apps, when a message is sent, it is immediately updated on the UI even before it is sent to the receiver, and most times, check marks are added to know when it is fully delivered.

The code below illustrates a task app where when a task is added to the list of tasks, it goes through an async function and is finally updated in the UI after 4s. We use the useOptimistic hook to render the task added immediately to the UI even before the 4s duration is done.

function App({ initialTasks }) {
  // State to hold the tasks
  const [tasks, setTasks] = useState(initialTasks);
  // Optimistic UI state for tasks
  const [optimisticTask, addOptimisticTasks] = useOptimistic(tasks);

  const inputRef = useRef(null);

  // Handle form submission
  async function handleSubmit(e) {
    if (inputRef.current == null) return;

    // Create an optimistic task
    const optimisticTask = {
      id: crypto.randomUUID(),
      title: inputRef.current.value,
 };

    // Add the optimistic task to the state
    addOptimisticTasks((prev) => [...prev, optimisticTask]);

    // Create a new task (simulating server creation)
    const newTask = await createTask(inputRef.current.value);
    // Add the new task to the state
    setTasks((prev) => [...prev, newTask]);
 }

  return (
    <>
      <form action={handleSubmit}>
        <label>Add New Task</label>
        <br />
        <input ref={inputRef} required />
        <br />
        <button>Create Task</button>
      </form>
      <ul>
        {/* Render the list of optimistic tasks */}
        {optimisticTask.map((task) => (
          <li key={task.id}>{task.title}</li>
 ))}
      </ul>
    </>
 );
}

// Function that simulates creating a task on the server
function createTask(title) {
  return delay(
 { id: crypto.randomUUID(), title: `${title} - in the server` },
    4000,
 );
}

// Function to create a delay
function delay(value, duration) {
  return new Promise((resolve) => {
    setTimeout(() => resolve(value), duration);
 });
}

export default App;

The image below shows the UI updated immediately. After 4 seconds, the task is replaced by the one on the server.

usetransition (1)

Previously, we would have handled it using the useState hook to manage the tasks to be displayed optimistically and update it with the actual task.

const [tasks, setTasks] = useState(initialTasks);
// Setting optimistic tasks using the useState hook
const [optimisticTasks, setOptimisticTasks] = useState(initialTasks);

const inputRef = useRef(null);

async function handleSubmit(e) {
  e.preventDefault(); // Prevent default form submission

  if (inputRef.current == null) return;

  const optimisticTask = {
    id: crypto.randomUUID(),
    title: inputRef.current.value,
 };

  // Add the optimistic task immediately
  setOptimisticTasks((prev) => [...prev, optimisticTask]);

  // Create the actual task with a delay
  const newTask = await createTask(inputRef.current.value);
  setTasks((prev) => [...prev, newTask]);
  setOptimisticTasks((prev) =>
    prev.map((task) => (task.id === optimisticTask.id ? newTask : task)),
 );
}

The code above uses the useState hook to add optimisticTasks for optimistic UI updates and its setter function, setOptimistic task, to update the state.

New API - use

The use API allows you to use Promises and async-await. It only takes a Promise, not a function, when the Promise is done running before the UI is updated. You can call this API within if statements and loops. You can also call this API within if statements and loops.

Let’s look at a code example. It has 2 async functions that simulate fetching data with a delay as you would normally have when sending the data to a server. It returns a list of authors and blogs after a delay of 4s and 2s, respectively, before updating the UI with these values.

import { use, Suspense } from "react";

// Create a delay
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

// Async function that fetches the list of blogs after the delay
const getBlogs = async () => {
  await delay(4000);
  return ["OpenReplay", "Devto", "Medium"];
};

// Async function that fetches the list of authors after a delay
const getAuthors = async () => {
  await delay(2000);
  return {
    isFetchUsers: true,
    authors: ["Mary Chidera", "Ugorji Marydera", "Ken Erics"],
 };
};

// Component to retrieve and display authors and blogs
function RetrieveAuthors({ authorsPromise, blogsPromise }) {
  // Use the authorsPromise to get the authors' data
  const { isFetchUsers, authors } = use(authorsPromise);

  console.log("Authors Promise Done: ", authors);

  let blogs;
  // If users are fetched, use the blogsPromise to get the blogs data
  if (isFetchUsers) blogs = use(blogsPromise);

  console.log("Blogs Promise is done :", blogs);

  return (
    <>
      <div>
        <h2>Authors</h2>
        <div>
          {/* Map over the authors array and display each author */}
          {authors.map((author, idx) => (
            <p key={idx}>{author}</p>
 ))}
        </div>
      </div>
      {blogs && (
        <div>
          <h2>Top Blogs</h2>
          <div>
            {/* Map over the blogs array and display each blog */}
            {blogs.map((blog, idx) => (
              <p key={idx}>{blog}</p>
 ))}
          </div>
        </div>
 )}
    </>
 );
}

// Main App component
function App() {
  // Create promises for authors and blogs
  const authorsPromise = getAuthors();
  const blogsPromise = getBlogs();

  return (
    // Use Suspense to handle loading states
    <Suspense fallback={<div>Loading Authors...</div>}>
      <RetrieveAuthors
        authorsPromise={authorsPromise}
        blogsPromise={blogsPromise}
      />
    </Suspense>
 );
}

export default App;

The image below shows the UI being updated only after all the Promises have finished loading.

use

Ref as a normal Prop

Refs allow DOM elements to be directly accessed and interacted with. They maintain their values between renders and don’t cause a component to re-render.

In React 19, you can now pass ref as a regular prop instead of using the forwardRef to pass a ref to a component.

Here’s the new implementation of the ref as a normal prop.

function NewRef({ ref }) {
  return <button ref={ref}>Click Me</button>;
}

//...
<NewRef ref={ref} />;

This is how we would have implemented it previously. As seen below, we use forwardRef to define the OldRef component. The forwardRef accepts props and a ref. The ref can now be accessed by the button component.

const OldRef = forwardRef((props, ref) => {
  return (
    <button ref={ref} {...props}>
      {props.children}
    </button>
 );
});

//...
<OldRef ref={ref} onClick={handleClick}>
 Click me
</OldRef>;

Action

In forms, you can now pass in the action prop. The prop can be an async function or a URL. The form would run the action prop when the form is being submitted.

We can also access the form inputs and elements by passing the formData to the function and using the get API.

function Form() {
  const addBlogName = (formData) => {
    const blogName = formData.get("blogName");
    console.log(`You submitted '${blogName}'`);
 };

  return (
    <form action={addBlogName}>
      <input name="blogName" />
      <button type="submit">Search</button>
    </form>
 );
}

export default Form;

Previously we would have accessed the name being passed using event.

const addBlogName = (event) => {
  event.preventDefault();
  const blogName = event.target.elements.blogName.value;
  console.log(`You submitted '${blogName}'`);
};

In the function addBlogName above, we directly accessed the value in the input element which is the blogName by using event.target.elements to access the element and its property.

Context as a Provider

You can now directly use the context as a provider in your code. Previously, whenever we wanted to call the context, we would have to do the contextName.Provider, then you pass in the children. Now we don’t need to do that anymore as we can directly call the name only without the .Provider.

In the code below, we created a context called lightModeContext and called it directly in the return statement.

//Context we created
const lightModeContext = createContext({ theme: "light" });

//New method of calling the context directly as a provider
function App({ children }) {
  return <lightModeContext value="dark">Children</lightModeContext>;
}

Previously, we would have needed to access the context we created by calling the lightModeContext.Provider.

//old method of calling the context with the .Provider
function App({ children }) {
  return <lightModeContext.Provider value="light"></lightModeContext.Provider>;
}

Support for Document Metadata

React 19 supports rendering metadata tags in the component. Previously, we would have done this using third-party libraries like react-helmet.

In the code below, we directly update the title and meta elements in the App component.

function App({ post }) {
  return (
    <article>
      <h1>{post.title}</h1>
      {/*meta tags */}
      <title>{title}</title>
      <meta name="description" content="post" />
      <meta name="keywords" content={post.keywords} />
    </article>
 );
}

Here’s an example of how we would have done it in earlier versions using the react-helmet library.

import React from 'react';
import { Helmet } from 'react-helmet';

function App({ post }) {
  return (
    <article>
      <h1>{post.title}</h1>
      <Helmet>
        <title>{post.title}</title>
        <meta name="description" content={post.description} />
        <meta name="keywords" content={post.keywords} />
      </Helmet>
      {/* Other content of your component */}
    </article>
 );
}

export default App;

In the code above, to set the title and meta elements, we wrapped them in a Helmet component from the react-helmet library.

Better Error Reporting

Error reporting has never been better. A new change to error output in React 19 makes errors look less ugly and a little bit better. Instead of having duplicate errors in your console, React 19 would try to give you one error with all the regular details of the error.

The image below is an error that occurred in the ErrorComponent reported by React 18.

React 18 error

Here is an image that shows the same error as reported by React 19.

React 19 error

We can see that React 19 simplifies the error into one error output.

Conclusion

React 19 brings exciting new features that enhance the development experience. We’ve explored these new features, such as the compiler that solves re-rendering issues, the new hooks to enhance user experience, and better error reporting to provide more concise error messages.

Additional Resources

More resources for further reading include:

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay