MB
⚛️ React 19: Top New Features & Developer Benefits

⚛️ React 19: Top New Features & Developer Benefits

April 7, 2024

React is set to receive a substantial update with version 19, but before concerns about the learning curve for a new React version start to arise, let me share some reassuring news. React 19 focuses less on the code you need to write and more on reducing the amount of code necessary. This article will cover the key features and improvements introduced in React 19.

New Features Coming in React Version 19 – A Brief Look

react mind
react mind

🤖 React Compiler

Most of the features that are in react 19 are due to the compiler, so what does it do?

The React compiler transforms your React code into standard JavaScript. It now oversees re-renders, significantly enhancing your app's performance. This means less worry about performance optimization on your part. Essentially, React determines which components need optimization and when, as well as what should be re-rendered, simplifying the development process.

react compiler
react compiler

Idempotency in React

One of the foundational aspects that the React Compiler relies on is the principle of idempotency in React components. Idempotency, in this context, means that a React component should return the same output given the same inputs, without side effects such as mutating props or state. This principle is crucial for the compiler's optimization process, as it allows React to automatically re-render just the right parts of the UI when state changes, ensuring efficient updates and performance improvements.

Impact of the New React Version on Idempotency

Introducing the React Compiler in the upcoming version significantly impacts the enforcement and implications of idempotency in React applications. By adhering to the rules of JavaScript and React, including the idempotency of components, developers can leverage the compiler to automatically optimize their applications. This shift means that the manual memoization practices currently used to prevent excessive re-renders (such as useMemo, useCallback, and memo) could become less necessary or even obsolete, as the compiler takes over the optimization process.

memoization
memoization

Example of use performance hooks before React 19:

tsx

import { useCallback, useMemo, useState } from "react";

function NameFilterApp() {
const [names, setNames] = useState([
"Tommy",
"Arthur",
"Polly",
"John",
"Grace",
]);
const [filter, setFilter] = useState("");

const handleFilterChange = useCallback((event) => {
setFilter(event.target.value);
}, []);

const filteredNames = useMemo(
() =>
names.filter((name) => name.toLowerCase().includes(filter.toLowerCase())),
[names, filter],
);

const totalLettersCount = useMemo(
() => filteredNames.reduce((acc, name) => acc + name.length, 0),
[filteredNames],
);

return (
<>
<input
type="text"
value={filter}
onChange={handleFilterChange}
placeholder="Filter names..."
/>
<div>
<strong>Filtered Names:</strong>
{filteredNames.length > 0 ? filteredNames.join(", ") : "No results"}
</div>
<div>
<strong>Total Number of Letters:</strong> {totalLettersCount}
</div>
</>
);
}

export default NameFilterApp;

In this example:

  1. useState is used to track the current filter (filter) and the list of names (names).
  2. useCallback is utilized to optimize the function handling changes in the filter text field, to prevent unnecessary re-rendering.
  3. useMemo is employed to optimize calculations related to filtering the list of names and calculating the total number of letters in filtered names. This ensures that these calculations are only performed when names or filter changes, not on every render.


New compiler will optimize your app code automatically, so you can completely remove any performance hook from your code you previously had:

tsx

import { useState } from "react";

function NameFilterApp() {
const [names, setNames] = useState([
"Tommy",
"Arthur",
"Polly",
"John",
"Grace",
]);
const [filter, setFilter] = useState("");

const handleFilterChange = (event) => {
setFilter(event.target.value);
};

const filteredNames = names.filter((name) =>
name.toLowerCase().includes(filter.toLowerCase()),
);

const totalLettersCount = filteredNames.reduce(
(acc, name) => acc + name.length,
0,
);

return (
<>
<input
type="text"
value={filter}
onChange={handleFilterChange}
placeholder="Filter names..."
/>
<div>
<strong>Filtered Names:</strong>
{filteredNames.length > 0 ? filteredNames.join(", ") : "No results"}
</div>
<div>
<strong>Total Number of Letters:</strong> {totalLettersCount}
</div>
</>
);
}

export default NameFilterApp;

Without memoization

In react 19 you also no more need to use forwardRef.

component forwardRef
component forwardRef

We can now pass a ref as a prop and access it just like any other prop, which is a notable enhancement. Observe the change:

component without forwardRef
component without forwardRef

💪 Directives and Actions

directives
directives

'use client'

The 'use client' directive in React is used to mark code that should run on the client side. By adding 'use client' at the top of a file, you signal to React and compatible bundlers that the module, along with its dependencies, is intended for client-side execution. This is particularly useful in applications that leverage Server Components, allowing for a clear separation between code that runs on the server and code that runs on the client.

use case

A common scenario for 'use client' is in interactive components that require access to browser APIs or need to maintain their state between renders. For example, a component that includes interactive forms, animations, or access to the local storage must run on the client side.

tsx

'use client';

import { useState } from 'react';

function InteractiveComponent() {
const [count, setCount] = useState(0);

return (
<div>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}

InteractiveComponent

Components that directly manipulate the DOM or use browser-specific APIs, such as fetching geolocation, playing audio, or animations using the Web Animations API, should be marked to run on the client.

tsx

'use client';

import { useEffect } from 'react';

function GeolocationComponent() {
useEffect(() => {
navigator.geolocation.getCurrentPosition(position => {
console.log(position);
});
}, []);

return <p>Check the console for the current position.</p>;
}

GeolocationComponent

Limitations and Disadvantages

  • Code Separation: While the 'use client' directive allows for a clear distinction between client-side and server-side code, it also requires developers to manage this division, which can lead to increased complexity in larger applications.
  • Bundle Size Increase: Code marked to run on the client is sent to the browser, which can potentially increase the size of the client bundle, especially if large amounts of code are designated for client-side execution.
  • State Management: Client components are responsible for managing their own state, which can lead to duplication of state logic between the client and the server.

Despite these challenges, the 'use client' directive provides powerful capabilities for optimizing application performance and enhancing user experience by allowing interactive components to run on the client side. By making informed decisions about which parts of an application should operate on the client, developers can strike an optimal balance between performance and feature richness.

use client directive react doc

'use server'

The 'use server' directive allows server-side functions to be callable from the client side, marking them as Server Actions. By placing 'use server' at the top of an async function body, it signals that the function can be invoked by client-side code, making a network request to the server with serialized arguments.

Example of form Action

A common use case for 'use server' is to handle form submissions. For instance, a form that allows users to sign up for a newsletter could call a server action to add the email address to a database.

tsx

// SignUpForm.js (Client Component)
import { useState } from 'react';

function SignUpForm({ submitEmail }) {
const [email, setEmail] = useState('');

const handleSubmit = async (e) => {
e.preventDefault();
await submitEmail({ email });
};

return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<button type="submit">Sign Up</button>
</form>
);
}

// submitEmail.js (Server Action)
'use server';

export async function submitEmail({ email }) {
// Logic to add email to database
}

SignUpForm

Data Mutation

Server actions can also be used for data mutations, such as updating a user profile or posting a comment, where the mutation logic is encapsulated in a server function.

Limitations and Disadvantages

  • Asynchronous Execution: Since server actions require a network call, they are inherently asynchronous, which can introduce complexity in managing UI states and feedback for long-running operations.
  • Serialization Constraints: Arguments passed to server actions and their return values must be serializable. This limits the types of data that can be sent to and from the server, excluding functions, JSX elements, and other non-serializable types.
  • Security Considerations: Because server actions can be invoked with client-controlled inputs, it's critical to validate and sanitize inputs to prevent security vulnerabilities, such as SQL injection or cross-site scripting (XSS).
  • Limited by Network Latency: The responsiveness of server actions is subject to network conditions. This can affect the perceived speed of operations initiated by the user, especially in environments with slow or unstable internet connections.

Advantages

  • Reduced Client-Side Complexity: By offloading operations to the server, you can simplify client-side logic, especially for operations that involve sensitive data processing or complex transactions.
  • Access to Server-Side Resources: Server actions can utilize server-side resources, such as databases or third-party APIs, which are not directly accessible from the client.
  • Improved Security: Executing actions on the server allows for better control over security, as sensitive operations and data access can be more tightly controlled and monitored.

The 'use server' directive offers a powerful model for integrating server-side logic into React applications, enabling developers to build more efficient and secure web applications. By thoughtfully applying server actions, developers can enhance the user experience while maintaining the robustness and scalability of their applications.

use server directive react doc

Both directives facilitate the development of applications with a clear separation of concerns between client-side and server-side logic, enhancing efficiency and performance in React applications.

🪝 New React Hooks

The use() hook

use() hook
use() hook

The use() hook in React 19, introduces a flexible way to access resources like Promises or contexts within components. Unlike other hooks, it can be used inside loops and conditional statements. This hook is designed to work seamlessly with Suspense and error boundaries, providing a more efficient way to handle asynchronous data fetching and context management

Fetching data before React 19

tsx

import { useEffect, useState } from "react";

const FetchDataComponent = () => {
const [posts, setPosts] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);

useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch(
"https://jsonplaceholder.typicode.com/posts",
);
if (!response.ok) {
throw new Error("Data could not be fetched!");
}
const data = await response.json();
setPosts(data);
} catch (error) {
setError(error.message);
} finally {
setIsLoading(false);
}
};

fetchData();
}, []);

if (isLoading) {
return <div>Loading...</div>;
}

if (error) {
return <div>Error: {error}</div>;
}

return (
<div>
<h1>Test Posts</h1>
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
);
};

export default FetchDataComponent;
fetch data before react 19


In this component, we perform the following steps:

  1. State Definition: We use the useState hook to create states for storing posts, the loading state (isLoading), and any potential errors (error).
  2. Data Fetching: We use the useEffect hook to perform an asynchronous fetch request to the API after the component has loaded. The fetchData function is asynchronous, allowing the use of await to wait for network responses.
  3. State Management: Depending on the network response, we update the state of the posts, loading, and error. We use try...catch to handle errors during the request.
  4. Rendering: Based on the state, the component renders information about loading, error, or the list of posts.

Fetching data after React 19 with the use() hook

tsx

// 1. Import React Hooks and Components
import { use, Suspense } from "react";

// 2. Define the Data Fetching Function
// This function fetches posts from a remote API and returns a promise.
const fetchPosts = () => {
return fetch("https://jsonplaceholder.typicode.com/posts")
.then((response) => {
if (!response.ok) {
throw new Error("Data could not be fetched!");
}
return response.json();
});
};

// Component that uses the `use` hook to fetch and display posts
const FetchDataComponent = () => {
// 3. Use the `use` Hook to Fetch Data
// The `use` hook is called with the promise returned by `fetchPosts`.
const postsPromise = fetchPosts();
const posts = use(postsPromise);

// 5. Map Over the Fetched Data
// This renders a list of posts by mapping over the array of fetched posts.
return (
<div>
<h1>Test Posts</h1>
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
);
};

// 6. Render the Component Tree
// The `App` component that renders the `FetchDataComponent` within a `Suspense` component.
export default function App() {
return (
// 4. Handle Loading State with `Suspense`
// `Suspense` shows a fallback UI while the data is being fetched.
<Suspense fallback={<div>Loading...</div>}>
<FetchDataComponent />
</Suspense>
);
}

fetch data react using the use() hook in react 19


In this approach, using new use() hook we perform the following steps:

  1. Import React Hooks and Components: The code starts by importing the necessary hooks and components from React. This includes the custom use hook and the Suspense component.
  2. Define the Data Fetching Function: A function named fetchPosts is defined outside the React component. This function is responsible for fetching data from a remote API (https://jsonplaceholder.typicode.com/posts). It uses the fetch API to make a GET request and returns a promise that resolves with the data or rejects with an error.
  3. Use the use Hook to Fetch Data: Within the FetchDataComponent, the use hook is called with the promise returned by fetchPosts. The use hook reads the value of this promise. It's worth noting that the actual implementation of how the use hook is used with a promise might vary depending on the specifics of your project setup.
  4. Handle Loading State with Suspense: The FetchDataComponent is wrapped in a Suspense component in the parent App component. The Suspense component is used to display a fallback UI (in this case, <div>Loading...</div>) while the promise is in the pending state. Once the promise resolves, the fallback UI is replaced with the FetchDataComponent's UI, displaying the fetched data.
  5. Map Over the Fetched Data: Inside FetchDataComponent, the fetched data (assumed to be an array of posts) is mapped over to render a list of posts. Each post is displayed in a list item (<li>) with its title.
  6. Render the Component Tree: Finally, the App component (containing the Suspense and FetchDataComponent) is rendered. When the App component is rendered, it initially shows the loading state. Once the data is fetched and the promise resolves, the component displays the list of posts.

Read context before React 19

In this example, ThemeContext.Provider enables passing the value "dark" to all components within the tree that consume this context. ThemeComponent reads the context value using useContext.

tsx

import React from 'react';
import { ThemeContext, ThemeComponent } from './Context';

// The main application component with a context provider
const App: React.FC = () => {
return (
<ThemeContext.Provider value="dark">
<ThemeComponent />
</ThemeContext.Provider>
);
}

export default App;
app.ts

tsx

import React, { createContext, useContext, ReactNode } from 'react';

// Creating a Context with a default value of 'light'
export const ThemeContext = createContext<string>('light');

interface ThemeComponentProps {
children?: ReactNode;
}

// Component using useContext
export const ThemeComponent: React.FC<ThemeComponentProps> = () => {
const theme = useContext(ThemeContext);
return <div>Current theme: {theme}</div>;
}

context.tsx

In this example, ThemeContext.Provider enables passing the value "dark" to all components within the tree that consume this context. ThemeComponent reads the context value using useContext.

Read context after React 19

Now, instead of useContext(), we will have use(context).

tsx

import React, { createContext, ReactNode, use } from 'react';

// Creating a Context with a default value of 'light'
export const ThemeContext = createContext<string>('light');

interface ThemeComponentProps {
children?: ReactNode;
}

// Component using the new use() hook
export const ThemeComponent: React.FC<ThemeComponentProps> = () => {
const theme = use(ThemeContext);
return <div>Current theme: {theme}</div>;
}

context.tsx

Advantages of the use Hook

  • Asynchronous Resource Loading: Seamlessly load different resources asynchronously, improving performance and user experience.
  • Simplifies Data Fetching: Replaces the useEffect hook for data fetching tasks, offering a more straightforward approach to making API requests and handling responses.
  • Context Data Access: It can replace useContext for accessing context data, reducing boilerplate and simplifying context management.

Practical Applications

  • Data Fetching: The use hook makes fetching data from an API more intuitive. Coupled with React Suspense, it allows for displaying a fallback UI while data is being loaded, and then seamlessly updating the UI once data fetching is complete.
  • Context Management: Simplifies reading context data by replacing useContext with use. This makes it easier to consume context in components, further reducing the complexity of React applications.

official documentation of use() hook

The useFormStatus() hook

useFormStatus()
useFormStatus()

The useFormStatus hook in React provides insightful status information about form submissions, which is crucial for handling form states and user interactions intelligently. This hook is specifically designed to be used within components that are rendered as part of a form. It returns a status object containing details about the form's submission state.

typescript

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

How It Works

To effectively utilize the useFormStatus hook, your component, such as a Submit button, needs to be part of a <form> element. The hook taps into the form's submission process, offering properties like pending to indicate if the form is currently being submitted.

Usage

  • Parameters: useFormStatus does not accept any parameters.
  • Returns: The hook returns a status object with several properties:
    • pending: A boolean indicating if the form is in the process of submitting. When true, it suggests that the form submission is underway.
    • data: An object adhering to the FormData interface, encapsulating the data being submitted by the form. This will be null if no submission is active or if the component isn't within a form.
    • method: A string, either 'get' or 'post', representing the HTTP method the form uses for submission. The method is determined by the method attribute of the form, defaulting to 'get' if unspecified.
    • action: This refers to the function provided to the form's action prop. It will be null if the form is not wrapped by a parent <form>, if the action prop is set to a URI, or if no action prop is provided.

Important Considerations

  • Form Dependency: It's crucial to call useFormStatus from within a component that resides inside a <form>. The hook is designed to provide status information specifically for a parent <form> and will not work outside this context.
  • Scope of Status Information: The hook exclusively tracks the submission status of a parent <form>. It does not extend this functionality to forms nested within the same component or child components.

Example:

tsx

import { useFormStatus } from "react-dom";
import { submitForm } from "./actions.js";

function Submit() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? "Submitting..." : "Submit"}
</button>
);
}

function Form({ action }) {
return (
<form action={action}>
<Submit />
</form>
);
}

export default function App() {
return <Form action={submitForm} />;
}
useFormStatus

By integrating useFormStatus into your React forms, you can enhance user feedback and control mechanisms during the form submission process, offering a more responsive and informative user experience.

The useFormState() hook

The useFormState hook, offers an innovative approach to managing form state based on the outcomes of form actions. This hook is particularly beneficial when used alongside React Server Components, enhancing the interactivity of forms even before client-side JavaScript fully kicks in.

What useFormState Offers

useFormState enables the creation of component state that updates in response to form actions. By passing an action function and an initial state to useFormState, you receive a new form action and the latest state, which can then be utilized within your form components.

How to Use useFormState

Let's define a simple action function that simulates a form submission, which can either succeed or fail based on the input data. Then, we'll update the StatefulForm component to display messages for success and error cases.

Form Action for Success and Error Simulation

typescript

// actions.js
// Simulate a form submission that can either succeed or fail.
export async function submitForm(previousState, formData) {
try {
// Simulate checking formData for success or failure criteria.
if (formData.get('exampleField') === 'success') {
return { status: 'success', message: 'Form submitted successfully.' };
} else {
throw new Error('Form submission failed.');
}
} catch (error) {
return { status: 'error', message: error.message };
}
}

submitForm

StatefulForm Component with Success and Error Feedback

tsx

import React from 'react';
import { useFormState } from 'react-dom';
import { submitForm } from './actions';

function StatefulForm() {
const [state, formAction] = useFormState(submitForm, { status: 'idle', message: '' });

const handleSubmit = (event) => {
event.preventDefault();
const formData = new FormData(event.target);
formAction(formData);
};

return (
<form onSubmit={handleSubmit}>
<input name="exampleField" placeholder="Type 'success' for success" />
<button type="submit">Submit</button>

{/* Feedback based on the form's submission state */}
{state.status === 'success' && <p style={{ color: 'green' }}>{state.message}</p>}
{state.status === 'error' && <p style={{ color: 'red' }}>{state.message}</p>}
</form>
);
}

export default StatefulForm;

StatefulForm

Explanation of the Code

  • Action Function (submitForm): This function simulates a form submission process. It returns a state object containing a status and a message. The status can either be success or error, determined by the submitted form data.
  • Form Component (StatefulForm):
    • Initializes form state using useFormState, with an initial state indicating that the form is idle.
    • Renders a simple form with an input field and a submit button. The form submission is handled by handleSubmit, which creates a FormData object and passes it to formAction.
    • Displays feedback messages based on the form's submission state. Success messages are shown in green, while error messages are displayed in red.

This example showcases how useFormState can be utilized to manage form submission feedback dynamically, improving user experience by providing immediate and relevant responses to their actions.

The useOptimistic hook

useOptimistic() hook
useOptimistic() hook

The useOptimistic hook in React provides a powerful way to make UIs feel more responsive by optimistically updating the interface ahead of long-running actions, like network requests. This approach is especially useful in scenarios where immediate feedback to user actions is desired, such as submitting forms or updating data.

How useOptimistic Works

useOptimistic allows you to temporarily update the UI based on what you expect to happen, assuming the action will succeed. It takes a piece of state and an update function (updateFn) that defines how to merge the current state with an optimistic update. This hook returns two items:

  • optimisticState: The state that includes the optimistic update.
  • addOptimistic: A function to call with an optimistic update value.

Example with useOptimistic

Let's create a simple example demonstrating optimistic UI updates when sending a message. The UI will immediately show the message as "Sending..." until the action completes.

tsx

import React, { useState } from 'react';
import { useOptimistic } from 'react';

function MessageSender() {
const [messages, setMessages] = useState([]);
const [messageInput, setMessageInput] = useState('');

const [optimisticMessages, addOptimistic] = useOptimistic(messages, (currentMessages, optimisticMessage) => {
// Merge the current messages with the optimistic update
return [...currentMessages, optimisticMessage];
});

const sendMessage = async (message) => {
addOptimistic({ id: Date.now(), text: message, status: 'Sending...' });
try {
// Simulate a network request to send the message
await new Promise(resolve => setTimeout(resolve, 1000));
// After the request completes, update the actual state
setMessages(prev => [...prev, { id: Date.now(), text: message, status: 'Sent' }]);
} catch (error) {
console.error('Failed to send message:', error);
}
};

const handleSubmit = (event) => {
event.preventDefault();
sendMessage(messageInput);
setMessageInput('');
};

return (
<div>
<form onSubmit={handleSubmit}>
<input
value={messageInput}
onChange={(e) => setMessageInput(e.target.value)}
placeholder="Type a message"
/>
<button type="submit">Send</button>
</form>
<ul>
{optimisticMessages.map(msg => (
<li key={msg.id}>{msg.text} - {msg.status}</li>
))}
</ul>
</div>
);
}

MessageSender

Code Explanation

  1. State Initialization: The component maintains two state variables: messages for the actual messages and messageInput for the current input value.
  2. Optimistic State: useOptimistic is used to create an optimisticMessages state, which combines the actual messages with any optimistic updates.
  3. Sending Messages: The sendMessage function optimistically updates the UI using addOptimistic, immediately showing the message as "Sending...". It then simulates a network request and, upon completion, updates the messages state to reflect that the message has been sent.
  4. UI Rendering: The component renders an input form for messages and a list of messages. The list reflects the optimisticMessages state, showing messages as "Sending..." optimistically until the simulated request completes.

This example illustrates how useOptimistic can enhance user experience by providing immediate feedback on actions whose outcomes are expected but have not yet been finalized, making the application feel faster and more responsive.

Here is a summary of the 3 form hooks we learned about

new form hooks
new form hooks


Summary

React 19 introduces a suite of features and improvements designed to enhance developer experience and application performance. At the heart of these advancements is the React Compiler, which optimizes re-renders and overall app efficiency, significantly reducing the need for manual optimization by developers. This new version aims to streamline the development process, allowing for more readable and concise code.

Key Highlights of React 19:

  • React Compiler: Central to React 19, it transforms React code into optimized JavaScript, automating performance optimizations and minimizing unnecessary re-renders. This advancement lessens the reliance on manual memoization techniques like useCallback(), useMemo(), and memo().
  • Simplified Development: The compiler's ability to handle optimization enables developers to write more straightforward component code. An example provided shows the evolution from a version requiring performance hooks for optimization to a cleaner version that eliminates these hooks, thanks to the compiler's optimizations.
  • Directives and Actions: React 19 introduces 'use client' and 'use server' directives for clearer separation between client-side and server-side logic, making codebases more efficient and organized. The 'use server' directive facilitates the implementation of server actions, simplifying data mutations and form submissions.
  • New React Hooks: A set of new hooks, including use() for accessing resources, useFormStatus() for form submission status, and useOptimistic for optimistic UI updates, further enhance the development experience by simplifying asynchronous data fetching, form state management, and responsive UI creation.

React 19's focus is not on introducing new syntax or concepts that steepen the learning curve, but rather on refining the framework to reduce boilerplate, improve performance, and make React more intuitive for developers. This version represents a significant leap towards more efficient, developer-friendly React applications.


Thank you for taking the time to read through this post. I hope you found the insights shared both valuable and thought-provoking. While this space doesn’t currently feature a comment section, I deeply appreciate your engagement and interest.

May you continue to grow, learn, and inspire those around you.

Cya!