29 avril 2024
What's New in React 19
6 minutes de lecture
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.