AccueilClientsExpertisesBlogOpen SourceContact

29 avril 2024

What's New in React 19

6 minutes de lecture

What's New in React 19
🇫🇷 This post is also available in french

More than 2 years after its last official release, the team at Meta has announced React version 19 in beta. Let's explore the major updates introduced in this version.

New use Function

The use function allows us to retrieve values from a promise or a context. Unlike a hook, it can be called conditionally without causing an error.

Usage with a Promise

When used with a promise, the use function comes along with the Suspense component. The rendering will be suspended while the promise is being resolved (or rejected). For example, rendering a list coming from an API:

// App.tsx
import { Suspense } from 'react'
import Users from './Users'

function App() {
  const usersPromise = fetch('https://jsonplaceholder.typicode.com/users').then((res) => res.json())

  return (
    <Suspense fallback={<div>Loading</div>}>
      <Users usersPromise={usersPromise} />;
    </Suspense>
  )
}

export default App
// Users.tsx
'use client'
import { use } from 'react'

const Users = ({ usersPromise }: { usersPromise: Promise<{ id: number; name: string }[]> }) => {
  const users = use(usersPromise)

  return (
    <ul>
      {users.map((user) => {
        return <li key={user.id}>{user.name}</li>
      })}
    </ul>
  )
}

export default Users

In this example, the promise is passed from a Server Component to a Client Component. In this case, it is necessary to ensure that the promise resolves serializable data types. It's recommended to create the promise within the server component and resolve it with use in the client component. For server components, the use of async / await is advocated over using use, which will cause a re-render after the promise resolves.

Error Handling

For error handling, we will use the error boundary concept.

import { Suspense } from 'react'
import Users from './Users'
import { ErrorBoundary } from 'react-error-boundary'

function App() {
  const usersPromise = new Promise((resolve, reject) => {
    setTimeout(() => reject(), 1000)
  })

  return (
    <ErrorBoundary fallback={<div>An error has occurred</div>}>
      <Suspense fallback={<div>Loading</div>}>
        <Users usersPromise={usersPromise} />;
      </Suspense>
    </ErrorBoundary>
  )
}

export default App

Usage with a Context

The use function can also be used with a React context. To do this, simply pass our context as an argument to our function.

import { createContext, use } from 'react'

const TodoContext = createContext<string[]>([])

function Todos() {
  const todos = use(TodoContext)

  return (
    <ul>
      {todos.map((todo) => {
        return <li key={todo}>{todo}</li>
      })}
    </ul>
  )
}

function App() {
  return (
    <TodoContext value={['Make a React 19 article', 'Contribute to Next-Admin']}>
      <Todos />
    </TodoContext>
  )
}

export default App

It's also worth mentioning a new feature of React in this example: it's no longer necessary to use MyContext.Provider to instantiate the provider of our context. MyContext alone is sufficient.

Updates for Refs

Refs have received a few updates. For context, a ref is a prop allowing to get the instance of an element. Currently, to pass a ref to an element coming from a functional component, you need to wrap it with the forwardRef function. In version 19, ref becomes a prop of a functional component, and using forwardRef is no longer necessary. However, the behavior does not change for class components.

import { forwardRef } from 'react'

const ComponentWithRef = forwardRef((props, ref) => {
  return <p ref={ref}>Hello</p>
})

const ComponentWithRefProp = ({ ref }) => {
  return <p ref={ref}>Hello</p>
}

Additionally, it is now possible to know when an element is removed from our DOM through a cleanup function in our ref.

<div
  ref={(divRef) => {
    myRef.current = divRef

    // Executed when the node is removed from the DOM
    return () => {
      console.log('Div removed')
    }
  }}
>
  My div
</div>

New useOptimistic Hook

In the context of an asynchronous request involving data mutation (such as an editing form), there might be a need to reflect the changes made instantly without waiting for the server's response. This is a pattern found, for example, in react-query. The hook takes two arguments:

  • the "real" state, i.e., the state coming from our data source
  • a function to update the "temporary" state, i.e., our displayed state until the data source has returned its new state.
import { useOptimistic, useState } from 'react'

type Todo = {
  label: string
  isSending: boolean
}

function Todos({ todos, sendTodo }: { todos: Todo[]; sendTodo: (todo: string) => Promise<void> }) {
  const [optimisticTodos, addTodo] = useOptimistic(todos, (state, newValue) => [
    ...state,
    {
      label: newValue,
      isSending: true,
    },
  ])

  const onSubmit = async (formData: FormData) => {
    const todo = formData.get('todo')
    addTodo(todo)
    await sendTodo(todo)
  }

  return (
    <div>
      <ul>
        {optimisticTodos.map((todo: Todo, index: number) => {
          return (
            <li key={index}>
              {todo.label} {todo.isSending && '(sending...)'}
            </li>
          )
        })}
      </ul>
      <form action={onSubmit}>
        <input type="text" placeholder="Todo" name="todo" />
        <button type="submit">Submit</button>
      </form>
    </div>
  )
}

function App() {
  const [todos, setTodos] = useState<Todo[]>([])

  const addTodo = async (todo: string) => {
    await new Promise((resolve) => setTimeout(resolve, 1000))
    setTodos((old) => [...old, { label: todo, isSending: false }])
  }

  return <Todos todos={todos} sendTodo={addTodo} />
}

export default App

New useActionState Hook

Taking our example above, we may, for instance, want to display a more impactful visual feedback during form submission when it's in a loading state. Though one could achieve this by scanning our optimisticTodos array for a isSending property set to true, useActionState offers a more elegant solution.

This hook allows us to execute a function (our action) and obtain a new state from this action. We can also access the loading state of our function, as well as a state that is closely linked to our action. Hence, our component can be greatly simplified:

import { useOptimistic, useState, useActionState } from 'react'

type Todo = {
  label: string
  isSending: boolean
}

function App() {
  const [todos, onSubmitTodo, isPending] = useActionState(
    /**
     * Our action
     */
    async (prevState: Todo[], newValue: string) => {
      await new Promise((resolve) => setTimeout(resolve, 1000))
      /**
       * New state
       */
      return [...prevState, { label: newValue, isSending: false }]
    },
    []
  )

  const [optimisticTodos, addTodo] = useOptimistic(todos, (state, newValue) => [
    ...state,
    {
      label: newValue,
      isSending: true,
    },
  ])

  const onSubmit = async (formData: FormData) => {
    const todo = formData.get('todo')
    addTodo(todo)
    await onSubmitTodo(todo)
  }

  return (
    <div>
      <ul>
        {optimisticTodos.map((todo: Todo, index: number) => {
          return (
            <li key={index}>
              {todo.label} {todo.isSending && '(sending...)'}
            </li>
          )
        })}
      </ul>
      <form action={onSubmit}>
        <input type="text" placeholder="Todo" name="todo" />
        <button type="submit" disabled={isPending}>
          Submit {isPending && '(loading...)'}
        </button>
      </form>
    </div>
  )
}

export default App

Meta Tag Support

Frequently in applications, there's a need to insert meta tags even though our component is situated at a level that doesn't allow access to the document’s head. Libraries like react-helmet were used to address this issue, but they behaved quite fragilely in server-side rendering. React 19 now allows rendering meta tags within a component, which will automatically be rendered within the document’s head, like so:

const App = () => {
  return (
    <main>
      <title>Todo list</title>
      <p>My todo list</p>
    </main>
  )
}

export default App

will render

<html>
  <head>
    <title>Todo list</title>
  </head>
  <body>
    <main>
      <p>My todo list</p>
    </main>
  </body>
</html>

Server Components and Server Actions

Already well implemented in Next.JS, Server Components and Server Actions will move to a stable state in version 19. We had the opportunity to discuss these two topics in more detail:

The Big Absence: React Compiler

A highly anticipated tool for this release that will not see the light of day yet is the React Compiler. To recap, its purpose is to relieve developers from worrying about memoization through the various useMemo and useCallback hooks. Instead, the React Compiler would automatically manage it. However, it seems the tool could arrive in open source very soon. It's a case to follow, but it's good to see the project hasn’t been abandoned and is being actively developed.

Conclusion

With version 19, React offers us new features that greatly simplify certain patterns related to asynchronous operations. It's becoming increasingly simple to provide a smooth and fast user interface while maintaining a more than adequate development experience. Although the various features presented were already available on the canary channel of version 18, they can now be tested on the canary, next, and beta channels, which all point to version 19 beta. You can find all the new features of version 19 in the React blog article.

18 avenue Parmentier
75011 Paris
+33 1 43 57 39 11
hello@premieroctet.com

Suivez nos aventures

GitHub
X (Twitter)
Flux RSS

Naviguez à vue