Mastering React 19: Exploring the Latest Features of the New React Version

Profile Picture of Will Eizlini
Will Eizlini
Full-stack Developer

React 18 was a major milestone in the framework’s journey, primarily because of the introduction of an interruptible, cancelable, concurrent rendering engine. This feature improved UI rendering, improved the interactions with Server Side React (SSR), and paved the way for many new features to be built upon React’s shiny new innards. 

React 19 fulfilled the promise set by React 18 by leveraging its interruptible and concurrent rendering engine to introduce Actions. These are a convention for handling Async data fetching within components. Building on the foundation of useTransition(), React 19 refines asynchronous interaction management and also introduces new hooks for Actions, form handling, and new features to React DOM’s <form> element. 

Along with Actions, React 19 also introduced Server Components and Server Actions. This was a major milestone and an important improvement. It is a lot to cover in one article, though, so watch out for Part II  that will explore Server Components and Server Actions in detail. 

In this article, we’ll look at the major new features and functionality introduced in React 19. We’ll also discuss upgrading from React 18. With that, let’s dig in!

Table Of Contents

React 19 Actions

Let’s start with Actions, arguably the most significant update in React 19. 

The React Blog defines Actions as follows: 

By convention, functions that use async transitions are called 'Actions'.

An Action in React 19 is not in itself a hook or an API. Rather, it represents a convention for consolidating common code paths when handling async calls originating from within a component. React 19’s implementation of Actions improves the boilerplate code for handling pending states, optimistic updates, and error handling associated with async calls to a server.

form submission example of an action in react 19
Actions can be thought of as a pipeline for handling asynchronous operations within a component.

Asynchronous Operations in React 19

useTransition

In React 18, useTransition was limited to handling synchronous updates. Now, React’s useTransition supports async functions, meaning you can handle transitions that involve server communication or other async operations. 

useOptimistic

Additionally, pending UI and error handling are improved with a new hook useOptimistic. This hook displays the “expected result” (or, optimistic updates) ahead of a round trip to the server, making the app feel faster and more responsive. useOptimistic also handles errors gracefully, by auto-magically reverting to the old value upon failure of the async call.

useActionState and useFormStatus

Along with useOptimistic,two more hooks were introduced: useActionState and useFormStatus.These new hooks work with React’s new <form> extensions and simplify form management.  Both useActionState and useFormStatus depend on React DOM’s extensions of <form>, which allow the action={} and form elements’ formAction={} to accept an async action function returned from useActionState and useFormStatus

All of these improvements shrink down our code, making it more readable when saving data with a call to a server.

table summarizing new and updated hooks in react 19
New hooks were introduced and older ones improved in React 19, including useTransition, useOptimistic, useActionState, and useFormStatus

New “Use” Hook

In my opinion, the use hook is one of the coolest new features in React 19. It allows for components to consume an async data resource via a promise and support for <Suspense> without needing a framework like Next.js or a module like ‘react-query’.

Base Code Example: Actions

Let’s start with a functional component that saves data to a server while managing the UI states and logic involved in an Action. We’ll optimize this example step by step in the following sections.

In the example below, users can view and edit their bio using a simple <textarea>. We use useState to track changes, manage the loading state during a server call, and handle errors if the save fails.

In many tutorials, examples work well for basic demos but can fall short when extending functionality for real-world scenarios. For this piece, I’ve chosen to explore a more practical example with the following considerations:

  1. The save button should be disabled when:
  2. The bio should load pre-existing data: Editing often starts with saved content rather than a blank slate.

This example is designed to reflect a more realistic use case, making it easier to build upon for real-world applications.

1 import { useState } from "react";
2 import { saveBio } from "../Api";
3
4 const Example1 = ({ currentBio, setCurrentBio }) => {
5 const [bio, setBio] = useState(currentBio);
6 const [pending, setPending] = useState(false);
7 const [error, setError] = useState(null);
8 const pristine = currentBio === bio;
9
10 const onSave = async () => {
11 setError(false);
12 setPending(true);
13 try {
14 await saveBio(bio);
15 setCurrentBio(bio);
16 } catch (serverError) {
17 setError(serverError.message);
18 }
19 setPending(false);
20 };
21
22 return (
23 <div className={"col"}>
24 <h3>Current Bio:</h3>
25 <p>{currentBio ? currentBio : <em>your bio is empty</em>}</p>
26 <textarea
27 onChange={(e) => {
28 setBio(e.target.value);
29 }}
30 placeholder="Describe yourself in a few words"
31 value={bio}
32 />
33 {error && <div className={"form-error"}>{error}</div>}
34 <button onClick={onSave} disabled={pending || pristine}>
35 {pending ? "saving" : pristine ? "saved" : "save"}
36 </button>
37 </div>
38 );
39 };
40 export default Example1;
41

The example is quite simple. We use react’s state to track the textarea value, the pending state, and the error state; pristine is simply a constant.

Renders by State: Pristine, Dirty, Pending, Error, Success

This simple example (and all of the examples further down) will render as follows, depending on the state of the component’s action:

UI states for action handling: pristine, dirty, pending, error, and success
Renders depending on state of the component’s action: pristine, dirty, pending, error, success.
Looking to hire a Remote React Developer?
We have pre-vetted remote developers ready to work on your React 19 project.
Hire React Developers

Async Functions in useTransition

Next, let’s make use of the hook useTransition and the ability of its function startTransition to accept an async function. This is a logical progression from the functionality of useTransition in React 18. 

In the code below, the highlighted lines show where we’ve implemented the same component using useTransition

1 import { useState, useTransition } from "react";
2 import { saveBio } from "../Api";
3
4 const Example2 = ({ currentBio, setCurrentBio }) => {
5 const [bio, setBio] = useState(currentBio);
6 const [error, setError] = useState(null);
7 const [pending, startTransition] = useTransition();
8 const pristine = currentBio === bio;
9
10 const onSave = () => {
11 startTransition(async () => {
12 setError(false);
13 try {
14 await saveBio(bio, false);
15 setCurrentBio(bio);
16 } catch (serverError) {
17 setError(serverError.message);
18 }
19 });
20 };
21 return (
22 <div className={"col"}>
23 <h3>Current Bio:</h3>
24 <p>{currentBio ? currentBio : <em>your bio is empty</em>}</p>
25 <textarea
26 onChange={(e) => {
27 setBio(e.target.value);
28 }}
29 placeholder="Describe yourself in a few words"
30 value={bio}
31 />
32 {error && <div className={"form-error"}>{error}</div>}
33 <button onClick={onSave} disabled={pending || pristine}>
34 {pending ? "saving" : pristine ? "saved" : "save"}
35 </button>
36 </div>
37 );
38 };
39 export default Example2;
40

You’ll see that, on line 7, we replace a useState for the pending state of our request for the useTransition. Additionally, on line 10 + 11 you’ll note our onSave is now a regular function, which executes startTranstition (which has our original async function as its argument).

Key Takeaways: useTransition Hook

You might be thinking, “this certainly isn’t saving me any lines of code.” And you’re right. In my opinion, there’s also not a big improvement in code clarity, either. 

The value of useTranstition, however, is that it allows our whole async code process to be encapsulated within a process which is optimized for React’s concurrent rendering engine. This means that waiting for a response won’t slow down the UI if an app needs to change state, update UI elements, or show animations while we handle the async call.

React 19 Hook: useOptimistic

The useOptimistic hook in React 19 is meant to keep an app’s UI feeling speedier and more responsive. It does so by allowing us to optimistically set the state of a value being submitted before receiving confirmation from the server. If the submission fails, the previous value is restored without requiring extra work…or what we like to call “automagically.”

I know, optimistic updating is nothing new. Many developers, the writer of this article included, have used optimistic updates for years because there are relatively few cases of connection issues that would throw an error in a simple case of editing a value.

While implementing an example, I was surprised to find that it wasn’t straightforward to handle this in an isolated component managing its own state without adding at least one more useState call. Initially, I hoped to rely solely on the form’s view state [bio, setBio], but no matter how I configured it, the displayed current value wouldn’t revert to its old value. Even worse, in some cases, the value of the <textarea> did revert upon a failed call, which is very bad UX!

The behavior I sought was this: 

  1. The current value of bio should show an optimistic update value.
  2. It should revert back upon failure. 
  3. The textarea should NOT revert back to the current value on failure to save, because then I have to type my changes all over again.

This was, in fact, the example that led me to set the state of the current bio outside of the components and have them passed down as props, which all of the examples do now. It makes a lot of sense for useOptimistic to work within this context, because that would be the bare-minimum real-world implementation, and more likely that there would be a variety of implementations using state management tools (redux being the most widely used).

1 import { useOptimistic, useState, useTransition } from "react";
2 import { saveBio } from "../Api";
3
4 const Example3 = ({ currentBio, setCurrentBio }) => {
5 const [bio, setBio] = useState(currentBio);
6 const [optimisticBio, setOptimisticBio] =
7 useOptimistic(currentBio);
8 const [error, setError] = useState(null);
9 const [pending, startTransition] = useTransition();
10 const pristine = currentBio === bio;
11 const onSave = (fail) => {
12 startTransition(async () => {
13 setError(false);
14 setOptimisticBio(bio);
15 try {
16 await saveBio(bio, fail);
17 setCurrentBio(bio);
18 } catch (serverError) {
19 setError(serverError.message);
20 }
21 });
22 };
23 return (
24 <div className={"col"}>
25 <h3>Current Bio:</h3>
26 <p>{optimisticBio ? optimisticBio : <em>empty</em>}</p>
27 <textarea
28 onChange={(e) => {
29 setBio(e.target.value);
30 }}
31 placeholder="Describe yourself in a few words"
32 value={bio}
33 />
34 {error && <div className={"form-error"}>{error}</div>}
35 <div className={"row"}>
36 <button onClick={() => onSave(false)}
37 disabled={pending || pristine}>
38 {pending ? "saving" : "save (success)"}
39 </button>
40 <button onClick={() => onSave(true)}
41 disabled={pending || pristine}>
42 {pending ? "saving" : "save (fail)"}
43 </button>
44 </div>
45 </div>
46 );
47 };
48 export default Example3;
49

This example includes two save buttons, which I used to test both success and failure scenarios.

Here’s what you’ll notice in the code above:

  • Initial Setup: On line 6, the value of the optimistic bio (displayed during submission and reverted upon failure) is initialized to the currentBio passed down as a prop. Since there’s no need to merge data (such as appending to a list), we don’t use a reducer function.
  • Save Function Behavior: The save function accepts a fail parameter, which determines whether the API call succeeds or fails. This is controlled by the true/false value passed to it. The save buttons utilize this feature in their onClick handlers to simulate and test both outcomes.
  • Optimistic Update: On line 14, within the save function, the optimistic bio is updated to the current value managed by the useState hook for the <textarea>.
  • Successful Save: On line 17, if the API call succeeds, the currentBio is updated using the state setter function provided through props.

This structure allows for realistic testing of success and failure scenarios while ensuring the UI reflects the appropriate states during the process.

Key Takeaways: useOptimistic

After testing, the behavior in this code worked exactly as expected. We see the displayed current bio, are able to edit and submit it, and while it’s submitting, the optimistic bio will take on the value of the edited value we are trying to save to the server. If the call fails, the displayed value reverts as expected. On success the value remains, and we propagate the now-saved value up to the state for the current bio.

Now that I have a better sense of how useOptimistic actually works, I think it’s a really useful tool for many use cases! Personally, I think I’ll continue to use it in certain contexts, though I don’t think all optimistic roads lead to this hook. Depending on how one manages state, it may be unnecessary. I hope this deep dive will help developers make that distinction.

React DOM’s <form>, useActionState, and useFormStatus

Another key feature in React 19 is the ability to use two new hooks, useActionState and useFormStatus, in conjunction with React 19’s upgrade of the <form> tag. In addition to being able to use a URL for the action, this means we can specify the async function we wish to use to call upon a form’s submission event. 

Here’s a practical example of the useActionState:

1const [state, formAction, isPending] = useActionState(fn, initialState, permalink?);

We’ll use this hook to set the action of our form. The state can be anything we want it to be, such as a value being edited. In the case of our code, we’ll use it to track the error. The requirement is that the action function returns that value that we are tracking. 

For the purpose of this example, we’ll ignore the isPending value. Instead, we’ll use a special SubmitButton component which will use useFormAction to get the formStatus, which already includes the pending value:

1const { pending, data, method, action } = useFormStatus();

Here is our working example:

1 import {useActionState, useOptimistic, useState} from "react";
2 import { saveBio } from "../Api";
3 import { useFormStatus } from "react-dom";
4
5 const SubmitButton = ({ pristine }) => {
6 const formStatus = useFormStatus();
7 return (
8 <button disabled={formStatus.pending || pristine}>
9 {formStatus.pending ? "saving" : "save"}
10 </button>
11 );
12 };
13
14 const Example4 = ({ currentBio, setCurrentBio }) => {
15 const [bio, setBio] = useState(currentBio);
16 const [optimisticBio, setOptimisticBio] =
17 useOptimistic(currentBio);
18 const pristine = currentBio === bio;
19
20 const onSave = async () => {
21 setOptimisticBio(bio);
22 try {
23 await saveBio(bio);
24 setCurrentBio(bio);
25 return null;
26 } catch (serverError) {
27 return serverError.message;
28 }
29 };
30
31 const [error, formAction] = useActionState(onSave, null);
32
33 return (
34 <form action={formAction}>
35 <div className={"col"}>
36 <h3>Current Bio:</h3>
37 <p>{optimisticBio ? optimisticBio : <em>empty</em>}</p>
38 <textarea
39 onChange={(e) => {
40 setBio(e.target.value);
41 }}
42 placeholder="Describe yourself in a few words"
43 value={bio}
44 />
45 {error && <div className={"form-error"}>{error}</div>}
46 <SubmitButton pristine={pristine} />
47 </div>
48 </form>
49 );
50 };
51 export default Example4;
52

On lines 25–27, you can see the return values of our asynchronous action function. In this case, the function tracks errors in the form and returns either null (for no error) or the server error, depending on the outcome. On Line 31, by using useFormStatus, we no longer need to declare the isPending variable (which would normally be the third return value from useActionState). This simplifies our code by consolidating form status tracking. Line 46 highlights the primary use case for useFormStatus: enabling the creation of reusable, form-agnostic components that can behave dynamically within a form’s context, such as displaying a loading indicator or disabling a submit button.

Remarks: useActionState, useFormStatus

I quite liked this way of programming updates. I don’t usually have the habit of wrapping my UI in a <form>, but React 19 is making me reconsider that approach. The hooks, useAction State and useTransition, can be used interchangeably, but useActionStatus clearly seems more useful in the context of a <form>,  when child components of the form consume the form state.

I was also able to integrate the use of useOptimistic in this example quite neatly, so I find this particular combination a winner!

React 19’s use hook

In my opinion, the ‘use’ hook was one of the most pleasant surprises in React 19. However, I was disappointed to find that many online articles and videos present use cases and code examples that are either misleading or just wrong. 

I have to admit, I came across many blogs that simply parrotted React Canary channel release notes while presenting code that doesn’t actually work. It’s surprising how often tech writers publish code without testing it. I don’t want to call out too many folks here, but if you tried the suggestions outlined in this Medium article and it didn’t work – well, that’s because they won’t work. My hope here is that I’ll share an example that is both useful and actually works. 


So what makes this hook “sexy”? Essentially, regular old React will allow you to consume any async data fetching algorithm and put it into a <suspense> component. In React 18, this was reserved only for frameworks and data fetching modules working closely with the React team.

Of course, you could hack a data request resource in React 18 and have it always throw a promise when the data was not yet ready, or return the data, or actually throw an error. <suspense> worked quite well, and i used a mock of this in my React 18 article.

React 19 has changed the suspense behavior to be more in line with how throwing is supposed to work (we are canonically supposed to  throw errors, not any old object). So anyone depending on React 18’s behavior with respect to suspense will find that their code no longer works.

React specifically makes the caveat however, that the promise invoked in use must not be created within the component that we’re trying to suspend and display. But when I tried it out without defining the async function, I sought help from the internet. Most results from Reddit, including this Reddit comment, suggested that it only works within larger frameworks.

However, what they mean by saying the “promise must not be created inside the component” is that the promise itself should not be defined within the component. Instead, the async function should be executed externally and referenced as a constant from within the component.

So, as long as the promise is created outside the component, it works like a charm!

Take in our examples, the loading of the bio when the app loads:

1 import { use, useState } from "react";
2 import { fetchBio } from "../Api";
3 import Example4 from "./Example4";
4
5
6 const bioPromise = fetchBio();
7
8 const Page = () => {
9 // const bio = use(fetchBio()); //<-- doesn't work!
10 const bio = use(bioPromise); //<-- works!
11 const [currentBio, setCurrentBio] = useState(bio);
12 return (
13 <>
14 <div className={"form-container"}>
15 <h2>Edit Your Bio Example 4 (forms)</h2>
16 <Example4
17 key={currentBio + "4"}
18 currentBio={currentBio}
19 setCurrentBio={setCurrentBio}
20 />
21 </div>
22 </>
23 );
24 };
25 export default Page;

And here is where we display it within a suspense element, and exemplifies the other use of the ‘use’ hook. We can use ‘use’ to invoke a context without using useContext. In fact, useContext has now, in React 19, become deprecated…

1 import ThemeContext from "../theme";
2 //import OldPage from "./OldPage";
3 const Main = () => {
4 //return <OldPage />;
5 const theme = use(ThemeContext);
6 return (
7 <Suspense
8 fallback={
9 <div className={"row"}>
10 <Loader color={theme.color} />
11 loading...
12 </div>
13 }
14 >
15 <Page />
16 </Suspense>
17 );
18 };
19 export default Main;

On Line 6 of the Page component  is where we correctly define the promise. In Main.js, line 5 we invoke use for the loading of a context to color our <Loader/> correctly

Remarks: “use” Hook

This example, refined after numerous attempts, is fully functional and reliable. The use of the use hook to invoke a context is a particularly elegant addition.

General Improvements in React 19

Of course, we see some general improvements in React 19 that I want to discuss a little here. To me, they follow a “cleaning up” pattern, perhaps redirecting the path of React to something with simpler APIs and interfaces.

Improvements with refs

forwardRef was a rather clunky interface that allowed components to expose their DOM nodes externally to a parent. You would have to wrap your entire functional component in a forwardRef and export the wrapped component, like this:

1 const Example5b = forwardRef((props, ref) => {
2 const { currentBio, setCurrentBio } = props;
3 const [bio, setBio] = useState(currentBio);
4 const [pending, setPending] = useState(false);
5 const [error, setError] = useState(null);
6 const pristine = currentBio === bio;
7 const id = useId();
8
9 // [...]
10
11 return (
12 <textarea
13 id={id}
14 onChange={(e) => {
15 setBio(e.target.value);
16 }}
17 placeholder="Describe yourself in a few words"
18 value={bio}
19 ref={ref}
20 />
21 );
22 });
23 export default Example5b;

refs are retrieved as the second argument in a forwardRef wrapped functional component. In the above example, we retrieve the ref in line 1 and deconstruct the props on line 2.
In React 19, we simply retrieve ref from the props. Now, we have no need to wrap our component:

1 const Example5 = ({ currentBio, setCurrentBio, ref }) => {
2 const [bio, setBio] = useState(currentBio);
3 const [pending, setPending] = useState(false);
4 const [error, setError] = useState(null);
5 const pristine = currentBio === bio;
6 const id = useId();
7
8 [...]
9
10 return (
11 <textarea
12 id={id}
13 onChange={(e) => {
14 setBio(e.target.value);
15 }}
16 placeholder="Describe yourself in a few words"
17 value={bio}
18 ref={ref}
19 />
20 );
21 };
22 export default Example5;

You can now access an internal DOM node from a parent component using a ref (see line 18). For example, the following code demonstrates how to use an effect to retrieve a child component’s id and display it:

1 const ref = useRef();
2 const [id, setId] = useState(null);
3 useEffect(() => {
4 if (ref.current) {
5 setId(ref.current.id);
6 }
7 }, [ref.current]);

In this example, we create a ref and pass it to the child component via the ref prop, regardless of whether the child is wrapped in another component. This ensures the ref always points to the desired DOM node.

1 <div className={"form-container"}>
2 <h2>Edit Your Bio Example 5 (forms)</h2>
3 <Example5
4 key={currentBio + "5"}
5 currentBio={currentBio}
6 setCurrentBio={setCurrentBio}
7 ref={ref}
8 />
9 <small>id = {id}</small>
10 </div>

You’ll always import useRef and create a ref to pass to a child component. In this example, the id retrieved from the ref ({id}) is rendered as a React-generated value, like “:r1:”, demonstrating how React handles internal IDs.

UI using ref to access and display internal id of child component in React

Using ref created by useRef, we can access and display the DOM node’s id (e.g., “:r1:”) directly from a parent component. This approach eliminates the need to wrap the child component in forwardRef to expose its DOM. It’s a much cleaner and simpler syntax.

Improved Error Reporting

In React 19, hydration errors are now displayed more clearly in the console, making debugging easier. Additionally, the latest release improves general error reporting by avoiding the rethrowing of errors. If your app relies on errors being rethrown, you may need to update your code to accommodate this change.

React 19 introduces a convenient feature allowing you to embed tags like <title>, <link>, <meta>, and <style> directly within your components. React will automatically hoist these tags into the <head> section of the DOM:

1 <div className={"form-container"}>
2 <h2>Edit Your Bio Example 5 (forms)</h2>
3 <Example5
4 key={currentBio + "5"}
5 currentBio={currentBio}
6 setCurrentBio={setCurrentBio}
7 ref={ref}
8 />
9 <title>Edit Your Bio!</title>
10 <style>{` small { color: blue; } `}</style>
11 <small>id = {id}</small>
12 </div>

In this example, we use the new React 19 <style> and <title> tag support. Here’s the result:

  • The document title is updated to “Edit Your Bio!”
  • The inline styles defined in the <style> tag apply correctly (e.g., the small text appears in blue).
Screenshot showing 'id = :r3:' in blue text and a browser tab with the title 'Edit Your Bio!' updated by React

When inspecting the DOM through Chrome DevTools, we can observe the <style> tag hoisted into the <head>:

1<style> small { color: blue; } </style><small>id = :r3:</small>

However, the <title> tag displays an unexpected behavior: React renders two visible <title> tags within the <head>, as shown below:

<title> tag in React 19 is rendered twice

I’m not sure why React rendered two visible <title> tags in the <head>. Looking at the HTML specifications for the <title> tag, we find this:

screenshot of error message for <title> tag in HTML specs

Other Improvements: Support for Async Scripts, Preloading, and Custom Elements

Along with the improvements so far discussed, React 19 introduces several new features to enhance performance and compatibility. 

Async Scripts

React 19 now supports asynchronous scripts, ensuring no duplication even when multiple component instances exist in the DOM. For instance:

1<script async={true} src="..." />

Support for Preloading

New React-DOM APIs allow you to preload specific resources, improving performance and load times.  You can get the details here.

Support for Custom Elements

React 19 has enhanced compatibility with Custom Elements and now passes all tests on the Custom Elements Everywhere benchmark:

React 19 beta passed all tests on Custom Elements Everywhere
React 19 has enhanced compatibility with Custom Elements and passes all benchmark tests.

How to Upgrade to React 19

As of December 2024, the React team announced that v19 was stable. While the release was later than many developers expected, some bloggers speculated that the delay was due to unexpected behavior in the <Suspense> component.

4-step flow chart for upgrading to React 19
A 4-step flow chart for upgrading your project to React 19.

1. Upgrade to React 18.3.1

If you haven’t already, start by upgrading your project to React 18.3.1, the latest version in the React 18 series. While largely identical to 18.2, this release introduces console warnings specifically designed to help you prepare for the React 19 upgrade. The React team recommends upgrading to 18.3.1, building, and running your project to identify potential issues before transitioning to React 19.

2. Upgrade to React 19

Before proceeding, ensure that Node.js and your npm package are up-to-date. Then, install React 19 with the following command:

1npm install --save-exact react@^19.0.0 react-dom@^19.0.0

If your project was created with create-react-app (CRA), you might encounter issues requiring the use of the –legacy-peer-deps flag. Note that CRA is no longer actively maintained and has known vulnerabilities. If possible, consider transitioning to alternative tools like Vite, which has been reported to work smoothly with React 19.

3. Run the codemods

React has partnered with Codemod.com to provide scripts that automate many of the code changes required for React 19. Running these codemods can significantly reduce the effort needed to address breaking changes:

1npx codemod@latest react/19/migration-recipe

You can check out the codemod react repository here.

4. Address Breaking Changes

React 19 introduces numerous breaking changes, many of which stem from deprecated APIs dating back to 2018. To ensure a smooth transition, review the complete list of breaking changes here. The codemod scripts can help clean up your codebase and streamline the migration process, so be sure to take full advantage of them.

Final Thoughts: Getting Started with React 19

React 19 is a significant update, introducing powerful new features and concepts. While we’ve covered many highlights, we’ve only scratched the surface of what’s new. This release introduces exciting hooks and async support, building on the foundation of Actions as a conceptual framework. Key additions include:

  • Enhancements to the async nature of effects, including updates to useTransition.
  • New hooks like useOptimistic, useActionStatus, and useFormStatus for better handling of async logic in forms.
  • The new use hook, which makes <Suspense> functionality easier to integrate.

We’ve also reviewed some basic improvements and tips to upgrade to React 19 with minimal friction.

While React 18 marked a significant leap in its internal architecture, React 19 stands out for its impactful new features. However, this is just the beginning—we haven’t yet explored React 19’s Server Components and Server Actions, which represent a major conceptual shift. These will be the focus of Part II in this article series.

Looking at React’s progression from 17 to 18 and now 19, the long-term vision becomes clearer. The React team appears to be moving toward server/client-agnostic code, enabling developers to write applications without relying on traditional server APIs or heavy client-side libraries. Over time, React may redefine what it means to be a full-stack developer, bridging the gap between server and client seamlessly.

Originally published on Dec 31, 2024Last updated on Feb 23, 2026

Key Takeaways

Should I migrate to React 19?

Yes—though upgrading should be done thoughtfully, and it’s important not to feel rushed. React 19 introduces exciting features like enhanced async handling, new hooks, and improved server component integration, which will likely benefit most projects in the long run. That said, migration isn’t always straightforward. Take the time to review your project’s dependencies, assess potential breaking changes, and test the upgrade in a development environment to ensure a smooth transition. Planning carefully now can save you headaches later while setting your project up to leverage the latest advancements.

What’s new in React 19?

React 19 introduces Actions for managing async data fetching within components, new hooks like useOptimistic, useActionState, and useFormStatus to streamline form handling, and the powerful use hook for async data fetching. It also enhances async script handling, preloading APIs, and Custom Elements support, while refining error reporting and expanding useTransition to support async functions.

What is the difference between React 18 and React 19?

React 19 builds on the foundations of React 18 with significant improvements to async data handling, introducing Actions for managing async operations within components and new hooks like useOptimistic, useActionState, and useFormStatus for better form management. The new use hook simplifies async data fetching, while useTransition now supports async functions. Additionally, React 19 enhances resource preloading APIs, improves support for Custom Elements, and refines error reporting. While React 18 introduced features like concurrent rendering, React 19 takes it further by making async workflows more powerful and efficient.

Looking to hire a Remote React Developer?

The Scalable Path Newsletter

Join thousands of subscribers and receive original articles about building awesome digital products. Check out past issues.