< Back to articles

React 19 FTW

Jakub BaierlJakub Baierl
July 03, 2024

I can’t remember a bigger tension in the React community since the release of React hooks in version React 16.8. Finally, after a few years, it has been done, specifically with version React 19. Is the boom justified, or is it just another JavaScript bubble triggering mass hysteria? Who am I to judge, I’ll leave the verdict to you. 👀

Before you dive into countless videos and blog posts, I would like to briefly point out that various sources contain incorrect examples and usage. I won’t point fingers – the API of new features changed on the fly, and therefore, the protagonists of these materials may not have had access to relevant information before the official release. Be cautious and look for materials released AFTER the official beta version release, which is April 25, 2024. 🏷️

I have tested most of the new features on specific examples, which you can also try out in this repository (sorry for the mess there, it was just quick real life testing), which served as a playground for me and I leave it available for you as well. We will show a few main points directly in the article, the rest you can find in the sample project.

If you would like to upgrade to the new version yourself, I recommend you go through the new migration guide, which contains both precise installation instructions and the use of prepared codemod scripts for automatic migration of your existing code, which I greatly appreciate.

🦹 What key features does the new version of React include?

Actions (formerly Server actions): These allow you to call asynchronous functions from the client, which are executed on the server. Using the “use server” directive, the framework creates a reference and, after calling from the client component, sends a request to the server where the function is executed. A typical example of calling such a function is changing the user’s name, where a simple form might look like this:

export const Form = () => {
  return (
    <form action={updateName}>
      <input type="text" name="name" />
      <button type="submit">{"Upravit jméno"}</button>
    </form>
  );
};

Since the native <form> in the example is directly (as we colloquially say in the team) “raped” into the React component, after the form submission, an action is directly called that modifies the new name in the database (which we’ve just faked with a simple Promise).

"use server";

export async function updateName(name?: string) {
  if (!name) {
    return { error: "Jméno je povinné" };
  }

  // Todo: Update name via API
  await new Promise((r) => setTimeout(r, 3000));
  // await db.users.updateName(name);
}

✨ Have a blast with those new React hooks!

Let’s stick with the same example and try to expand it with a submit button that will change according to the form’s state.

<form action={updateName}>
  <input type="text" name="name" />
  <SubmitButton />
</form>

A common real-life example is to change the submit button to a disabled state (and possibly change its text) after firing the form, so the user knows something is happening and at the same time cannot submit the form multiple times in a row. For this, we will use useFormStatus, which will evaluate in the context of which form it is located and return the state of that form.

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

  return (
    <button type="submit" disabled={pending}>
      {pending ? "Upravuju..." : "Upravit jméno"}
    </button>
  );
};

🚀 And we roll on…

We all probably know optimistic updates (if not, see an example here). In short – instead of waiting for the result of an API request, we show the user the new data immediately and after completing the API request, we either keep the data or revert the state otherwise.

React 19 came with a special hook just for this use case, which can be used in various ways. In the attached repository, you can try the example of a live chat (<OptimisticChat />), where messages are displayed immediately after sending and only after completing the request, information about their status on the API is displayed.

Image from Google.png

The usage is very simple, we extend the function after submitting the form (in this case formAction), with a method created using the useOptimistic hook, and thus we update the local message state immediately after sending the message (with the addition that each message will have a flag sending: true) and then only send the message with a request to the API.

async function formAction(formData: FormData) {
  addOptimisticMessage(formData.get("message"));
  formRef?.current?.reset();
  await sendMessage(formData);
}


const [optimisticMessages, addOptimisticMessage] = useOptimistic(
  messages,
  (state, newMessage) => [
    ...state,
    {
      text: newMessage,
      sending: true,
    },
  ]
);

Once we receive the result from the API, we will overwrite the local message state with the actual values, set the sending flag to false, and possibly add an error message if the message failed to save to the API.

const [messages, setMessages] = useState([
  { text: "First message!", sending: false, key: 1, error: null },
]);


async function sendMessage(formData: FormData) {
  const sentMessage = await deliverMessage(formData.get("message"));
  setMessages((messages: Message[]) => [
    ...messages,
    {
      text: sentMessage.message ?? formData.get("message"),
      error: sentMessage.error,
    },
  ]);
}

Awesome. 😍

😌 forwardRef? No more…

It’s not a biggie, but it’s sure to please everyone. With the new version of React, we can finally pass refs as props to child components just like we pass any other properties.

export const Form = () => {
  const inputRef = useRef<HTMLInputElement>(null);

  return (
    <form>
      <InputWithRef ref={inputRef} />
      <SubmitButton />
    </form>
  );
};

export const InputWithRef = ({ ref, ...rest }: InputRefProps) => {
  return (
    <>
      <label>Jméno</label>
      <input ref={ref} type="text" name="name" {...rest} />
    </> 
  );
};

🎉🎉🎉

🧾Document metadata support

We can finally get rid of react-helmet or similar workaround libraries for setting metadata, as we can now set metadata directly from components across the entire app, even in lazily loaded components. For example, for pages with dynamic content, it’s no problem to set metadata after downloading the relevant data by simply adding <title>{data.name}</title > to your component.

const UserDataComponent = () => {
  const data = useData();

  return (
    <>
      <h1>{data.name}</h1>
      <title>{data.name}</title>
    </>
  );
};

Likewise, you can lazy load stylesheets and other assets. 🏋️

💡And what about Server components?

Yes, in React 19, server components will be stable! Of course, it depends on which framework you use and how you want to utilize server components. Be aware that the previously mentioned use-server directive (which is only for Actions) is not for this use; you always need a server where server components will be processed, whether after a request is invoked from the client or already at build time.

However, we have the option to achieve a similar result in client components, where with the new use() hook, we can achieve conditional rendering of specific asynchronous components.

Let’s say we want to asynchronously download data from an API and display a given component only when the data is ready.

const fetchData = async () => {
  "use server";

  const res = await fetch("https://api.chucknorris.io/jokes/random");
  return res.json();
};

export const PromiseUseExample = () => {
  const requestPromise = fetchData();
  // Note: Ideally use with useMemo to cache the promise
  // const requestPromise = useMemo(fetchData, []);

  return <SuspensedComponent promise={requestPromise} />;
};

This hook, in collaboration with the Suspense API, ensures that the component will indeed render only when the data is available, unlike before when the component rendered with empty data and then again when the data state changed, and the developer had to manage when to display which part of the component and when not to.

const SuspensedComponent = ({ promise }: SuspensedComponentProps) => {
  const res = use(promise);

  return <p>{res.value}</p>;
};

const SuspensedComponentWrapper = ({ promise }: SuspensedComponentProps) => (
  <Suspense fallback={<p>čekám na data...</p>}>
    <SuspensedComponent promise={promise} />
  </Suspense>
);

At the same time, the use() hook is the only hook that you can call conditionally, for example:

if (something) {
  const res = use(promise);
}

And that pays off! 💰

📒 React Context and use hook?

From the new version, Context comes with a modified API named Context as a Provider, which slightly reduces the code because the defined context, as the name suggests, can be rendered directly as a provider (no more <Context.Provider>, which is of course still backward compatible).

const TestContext = createContext({});

export const ContextExample = () => {
  return (
    <TestContext value={{ name: "Nejlepší react verze" }}>
      <ContextComponent />
    </TestContext>
  );
};

And at the same time, using the new use() hook, we can access the context in any child component at any level of nesting.

const ContextComponent = () => {
  const res = use(TestContext);

  return <p>Context data: {res.name}</p>;
};

Other new features, improved (not just hydration) errors, and more usage examples are now available on the official website.

🤖 Don’t forget about the React Compiler!

Although the React Compiler is not part of React 19, its use is conditioned by this version. Therefore, it seems appropriate to briefly introduce it here, so we know what to look forward to.

Remember and use hooks like useMemo, useCallback, or memo itself introduced a few years ago?

…you can forget about them because the React Compiler promises to solve it for you!

Automatic memoization is a game changer that not only improves developer experience but also speeds up our apps by the promised 20 %. The condition for the proper functionality of the compiler is adherence to a set of rules known as the Rules of React.

At the same time, the new compiler also uses its own eslint plugin, which you must install. I highly appreciate the possibility of using the compiler only on part of the codebase using a configuration file:

const ReactCompilerConfig = {
  sources: (filename) => {
    return filename.indexOf('src/path/to/dir') !== -1;
  },
};

or by using the “use memo” directive where you want to let the compiler run.

For more information on installation and use, visit the official website.

🏆 Summary

The new version contains many useful features that are very easy and quick to adapt. As is customary with React, there is no breaking change here that would violate backward compatibility.

In the attached repository, you will find all the examples described above and some additional ones that did not fit into the blog.

To lighten things up at the end, I’ve prepared a simple tier list that serves as a point ranking of the given functionalities. You can be inspired by my rating, or make your own here.

Unnamed (2).png

If you are interested in more information with commentary, check out the video from the meetup or the presentation.

🗣️ Sources

React Compiler: In-Depth Beyond React Conf 2024 They made React great again? React Compiler React 19 - Dev Blog React 19 Is FINALLY Here New Hooks Explained React 19 - New features and updates Server components

Jakub Baierl
Jakub Baierl
Frontend Team LeadWhen Jakub is not entartaining others with his jokes, music is his life (either on the stage or in front of it) and he also sews T-shirts. In Ackee, he belongs to the old pros, so he has a lot of experience to share.

Are you interested in working together? Let’s discuss it in person!

Get in touch >