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.
Session Replay for Developers
Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — an open-source session replay suite for developers. It can be self-hosted in minutes, giving you complete control over your customer data.
Happy debugging! Try using OpenReplay today.
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.
The image below shows the time it takes for each re-render of the whole component in the transpiler.
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
- The useActionState Hook
- The useFormStatus Hook
- The useOptimistic Hook
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.
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.
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.
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.
Here is an image that shows the same error as reported by React 19.
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:
New hooks - useTransition, useOptimistic, useFormStatus, useActionState.
Top comments (0)