All The New Features In React

Tuan Tran VanTuan Tran Van
46 min read

React’s journey began in 2011 when Facebook engineers created it to manage the increasingly complex interface of their rapidly growing platform. React’s initial focus was on re-rendering only the necessary parts of the UI, leading to improved performance and smoother user experiences. Over the years, React has evolved significantly, incorporating new features like Hooks, concurrent rendering, and server-side rendering.

Today, we will list those new features together over several updates to React.

Let’s get started!!!

React 19

The release of React 19 brings a host of exciting new features and updates that enhance developer productivity, offer performance, and offer greater flexibility.

Automatic Memoization

Do you remember React Forget introduced by Huang Xuan at React Conf 2021?

Now, it’s here.

It’s a compiler that has been already applied in Instagram’s production environment. The React team plans to apply it in more platforms within Meta and will make it open-source in the feature.

Before using the new compiler, we used useMemo, useCallback, and memo to manually cache states to reduce unnecessary re-renders. Although this implementation is feasible, the React team believes it is not the ideal way they envision. They have been looking for a solution that allows React to automatically and only re-render the necessary parts when the state changes. After years of effort, the new compiler has successfully landed.

The new React compiler will be an out-of-the-box feature, representing another paradigm shift for developers. This is the most anticipated feature of v19.

Interestingly, the React team did not mention “React Forget“ when introducing the new React compiler, which led to a humorous comment from the community: They forget React Forget & forget to mention Forget in the Forget section 😂

React Server Components: Enhanced Support for Pre-Rendering Components

React Server Components, introduced experimentally in React 18, are getting a boost in React 19. They allow components to run on the server during the initial render, fetching data and performing other server-side logic before sending HTML to the client. This leads to faster initial page loads, improved SEO, and a better overall user experience.

The Problem With Traditional Client-Side Rendering

Traditional Client-Side Rendering (CSR) involves sending a minimal HTML file to the client, which downloads Javascript bundles and renders the application in the browser. This approach has several drawbacks:

  • Slower Initial Page Load: Users have to wait for the Javascript to download, parse, and execute before seeing the content, leading to a longer time to first contentful paint (TFCP) and a perceived slower loading experience.

  • Poor SEO: Search engines often struggle to index content rendered by Javascript, negatively impacting search engine optimization (SEO).

  • Increased Client-Side Processing: Data fetching and other logic on the client can strain devices, especially those with low power or poor network connections.

Benefits of React Server Components

RSCs address these issues by allowing components to execute on the server. This offers several key advantages:

  • Faster Initial Page Load: The server renders the initial HTML, including the content, reducing the amount of Javascript that needs to be downloaded and executed on the client. This leads to a significantly faster TFCP and a better user experience.

  • Improved SEO: Because the content is rendered on the server, search engines can easily index it, improving SEO.

  • Reduced Client-Side Javascript: By moving data fetching and other logic to the server, the size of the Javascript bundles sent to the client is reduced, further improving performance.

  • Direct Database Access: Server Components can directly access databases and other server-side resources without needing create API endpoints. This simplifies data fetching and reduces code complexity.

  • Improved Security: Sensitive operations, like database queries or access to environment variables, can be performed on the server, enhancing security.

How React Server Components Work

RSCs are rendered on the server and send a special data format (called the React Server Component Payload) to the client. This payload describes the component tree and its properties. The client-side React runtime then uses this payload to hydrate the HTML and make the application interactive.

Here’s a simplified illustration:

  1. Request: The user requests a page

  2. Server Rendering: The server renders the RSCs, fetching data and generating HTML and the React Server Component Payload

  3. Response: The server sends the HTML and the payload to the client.

  4. Client Hydration: The client-side React runtime hydrates the HTML using the payload, making the application interactive.

// Server Component (server-side)
async function ProductDetails({ productId }) {
  'use server'; // Marks this as a Server Component
  const product = await fetchProductData(productId); // Fetch data on the server
  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      {/* ... */}
    </div>
  );
}

// Client Component (client-side)
'use client'; // Marks this as a Client Component
import ProductDetails from './ProductDetails';

function ProductPage({ productId }) {
  return <ProductDetails productId={productId} />;
}

In this example, the ProductDetails component is a Server Component that fetches product data on the server and renders the HTML. The ProductPage component is a Client Component that renders the ProductDetails component.

Enhanced Support for Pre-Rendering

React 19 further enhances RSCs' pre-rendering capabilities by improving how they handle streaming and partial hydration. This leads to even faster initial page loads and a more seamless user experience.

In summary, the React Server Component provides a powerful new way to build React applications, offering significant performance improvements and a better developer experience. They enable efficient pre-rendering, improved SEO, and reduced client-side Javascript, making them a crucial tool for building modern web applications.

Are they the same as Server-Side Rendering?

That is a good question, I asked myself as well.

After all, we are talking about rendering components on the server. However, a small but very significant difference is that while SSR will render the entire page, resulting in the server returning all the HTML you need, server components will only render a single component and they will make the server return only THAT HTML.

Again, it is small but definitely important.

The difference means that for pages with many components, some of which are heavy and require a lot of processing (such as data loading), you can split the load and have both the server and the client work in parallel to render all components as fast as possible.

Of course, this is all almost completely transparent to you as the developer. There are some rules you can’t break, but overall, you are free to mix and match components.

The only limitations:

  • Server-side components can’t be imported by client-side components. After all, the server-side one might have servers-specific code that won’t work on the browser.

  • You can pass props between server and client components, but the data in them needs to be serializable. This is obvious if you think about it, after all there is no easy way to transfer a class or a date object between server and client. And whether you realize it or not, that’s what you are doing by sharing props, you are transferring data between both environments.

How do you define a server component in NextJs 13?

This is the best part, doing this is trivial.

To define a server component, simply add the ‘use client’ directive at the top of the file.

You only have to do that with the components you specifically want to be rendered on the client; otherwise, Next will decide where to render them based on the code you are using.

For example, if you don’t specify a directive, and inside your component you use useEffect or useState Next, it will still render it inside the browser.

However, if you write it like this, the component will be rendered on the back end.

async function getActivity() {
    let result = await fetch("https://www.boredapi.com/api/activity?type=recreational")
    const obj = await result.json()
    return { activity: obj.activity }
}

export default async function Bored () {

    let activity = await getActivity();

    return (
       <div><h1>
        Your activity
       </h1>
       <p>
        {activity.activity}
       </p>
       </div>)
}

This code defines an async function that will render the component. It also performs a async fetch operation to get some data from an external API.

The rendered output from this component and the app using it is the following:

The screenshot shows the dev tools on Firefox. You can see there are no XHR requests for the site. That’s because this component was rendered on the server.

You can’t even get the source code for this component on the dev mode, because all we get on the browser is the result.

Server Actions: Streamlining Server-Side Logic in React 19

React 19 introduces Server Actions, a new feature that allows you to run server-side logic directly from your React components without needing to set-up separate API endpoints. This approach simplifies server interactions, reduces the client-side Javascript payload, and improves performance by executing heavy or secure operations on the server.

Server Actions are especially useful for handling operations like form submissions, database mutations, and other server-only logic directly within the React component tree. They seamlessly integrate server-side execution into the React application flow.

How Server Actions Work

Server Actions work by marking specific functions as server-side with a ‘use server’ directive. This lets React know that the code should run on the server, not the client, which keeps sensitive logic off the user’s device and improves security.

Code Snippet: Using Server Actions in React 19

Here’s an example demonstrating how to use Server Actions to handle a form submission without setting up separate API endpoints:

// components/SubmitForm.js
'use server'; // This directive tells React that this function runs on the server

export async function submitForm(data) {
  // Simulate a server-side operation, like saving to a database
  console.log('Processing data on the server:', data);

  // Example of server-side logic
  const response = await fetch('https://api.example.com/save', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(data),
  });

  if (!response.ok) {
    throw new Error('Failed to save data on the server.');
  }

  return response.json();
}

Component Using the Server Action

// app/FormComponent.js
'use client'; // This is a client component

import { useState } from 'react';
import { submitForm } from '../components/SubmitForm'; // Import the server action

export default function FormComponent() {
  const [formData, setFormData] = useState({ name: '', email: '' });
  const [message, setMessage] = useState('');

  const handleSubmit = async (e) => {
    e.preventDefault();
    try {
      const result = await submitForm(formData); // Call the server action directly
      setMessage('Form submitted successfully!');
      console.log('Server response:', result);
    } catch (error) {
      setMessage('Error submitting form.');
      console.error('Error:', error);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={formData.name}
        onChange={(e) => setFormData({ ...formData, name: e.target.value })}
        placeholder="Name"
      />
      <input
        type="email"
        value={formData.email}
        onChange={(e) => setFormData({ ...formData, email: e.target.value })}
        placeholder="Email"
      />
      <button type="submit">Submit</button>
      {message && <p>{message}</p>}
    </form>
  );
}

React 19 Server Actions provide a streamlined way to manage server-side logic directly within your React components. This reduces the need for separate API layers and enhances overall application performance. By leveraging this feature, developers can create more responsive, secure, and maintainable applications.

Async Transitions: Simplifying State Changes

React 19 introduces Async Transitions, a powerful new feature designed to simplify the management of complex state updates that involve asynchronous operations, such as data fetching or animations. This feature builds upon the existing useTransition hook, providing a more elegant and efficient way to handle UI updates that depend on asynchronous results.

The Problem with Traditional State Transitions

Managing state updates that depend on asynchronous operations often requires complex logic and careful coordination of multiple state variables. This can lead to code that is difficult to read, maintain, and reason about.

Consider a scenario where you are fetching the data and updating the UI based on the result:

import { useState, useEffect } from 'react';

function MyComponent() {
  const [data, setData] = useState(null);
  const [isPending, setIsPending] = useState(false);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      setIsPending(true);
      try {
        const response = await fetch('/api/data');
        const json = await response.json();
        setData(json);
      } catch (err) {
        setError(err);
      } finally {
        setIsPending(false);
      }
    };

    fetchData();
  }, []);

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

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

  return <div>Data: {JSON.stringify(data)}</div>;
}

This code uses multiple state variables (data, isPending, error) to manage the asynchronous operation and its effects on the UI. This pattern can become even more complex when dealing with multiple asynchronous operations or more intricate UI updates.

Simplifying State Updates With Async Transitions

Async Transitions simplifies this process by providing a way to perform asynchronous operations within a transition. This allows React to handle the pending state and prioritize user interactions during the asynchronous operation.

The useTransition hook now supports asynchronous functions as the update parameter. This allows you to directly to perform asynchronous actions within a transition.

import { useState, useTransition } from 'react';

function MyComponent() {
  const [data, setData] = useState(null);
  const [isPending, startTransition] = useTransition();

  const handleClick = () => {
    startTransition(async () => {
      try {
        const response = await fetch('/api/data');
        const json = await response.json();
        setData(json);
      } catch (err) {
        // Handle error
        console.error(err)
      }
    });
  };

  return (
    <div>
      <button onClick={handleClick} disabled={isPending}>
        Fetch Data
      </button>
      {isPending && <div>Loading...</div>}
      {data && <div>Data: {JSON.stringify(data)}</div>}
    </div>
  );
}

In this example, the asynchronous data fetching is wrapped within startTransition. React automatically manages the isPending state during the fetch. This results in cleaner code and a more streamlined approach to managing asynchronous state updates.

Key Benefits of Async Transitions

  • Simplified State Management: Reduces the need for multiple state variables to manage asynchronous operations.

  • Improved User Experience: Ensures that user interactions are prioritized during asynchronous operations, preventing the UI from becoming unresponsive.

  • Cleaner Code: Makes code that deals with asynchronous state updates more concise and easier to read.

  • Built-in Pending State Handling: React automatically manages the pending state, reducing the boilerplate code.

  • Integration with Suspense: Async Transitions work well with Suspense for more declarative loading states.

Async Transitions provide a powerful and elegant way to manage complex state changes involving asynchronous operations. By simplifying state management and improving user experience, Async Transitions contribute to building more performant and maintainable React applications.

useOtimistic: Managing optimistic updates with ease

React 19 introduces the useOtimistic hook, a powerful tool for simplifying the implementations of optimistic updates. Optimistic updates drastically improve user experience by providing the illusion of instant UI changes, even before the corresponding server-side operations are confirmed. This creates a more responsive and fluid interaction.

The Problem with Traditional Optimistic Updates

Traditionally, implementing optimistic updates in React involved manual state management. This often led to several challenges:

  • Complex State Logic: Developers had to manually manage the optimistic state alongside the actual data, often requiring intricate logic to update and revert changes. This could lead to complex and hard-to-maintain code.

  • Potential for Inconsistencies: Manually managing states increases the risk of inconsistencies between the UI and the actual data. For instance, forgetting to revert an optimistic update after a failed server request could lead to a desynchronized user interface.

  • Boilerplate code: Implementing optimistic updates often requires a significant amount of boilerplate code to handle state updates, error handling, or reverting changes. This increases development time and makes the code less readable.

  • Difficulty in Handling Complex Updates: When dealing with more complex data structures or multiple concurrent updates, manually managing the optimistic state becomes even more challenging, increasing the likelihood of bugs.

Consider a simple example of adding a new item to a list. Without useOptimistic, you might have code that looks something like this:

import { useState } from 'react';

function ItemList() {
  const [items, setItems] = useState([]);
  const [isAdding, setIsAdding] = useState(false);

  const addItem = async (newItem) => {
    setIsAdding(true); // Indicate loading state
    setItems([...items, { ...newItem, tempId: Date.now() }]); // Optimistic update

    try {
      const response = await fetch('/api/items', { method: 'POST', body: JSON.stringify(newItem) });
      const data = await response.json();
      setItems(items.map(item => item.tempId === newItem.tempId ? data : item)); // Replace temp item
    } catch (error) {
      // Handle error and revert
      setItems(items.filter(item => item.tempId !== newItem.tempId));
      console.error("Error adding item:", error);
    } finally {
      setIsAdding(false);
    }
  };

  // ...
}

This code, even in simplified form, demonstrates the complexity involved in manually managing optimistic updates.

Simplifying Optimistic Updates With useOptimistic

useOptimistic addresses these issues by providing a declarative and streamlined way to manage optimistic updates. It simplifies the process by abstracting away the complexities of manual state management.

The useOptimistic hook takes 2 arguments:

  1. initialValue: The initial value of the state

  2. updateFn: The function that receives the current optimistic value and the update argument and returns the new optimistic value.

It returns an array containing:

  1. optimisticValue: The current optimistic value

  2. addOptimistic: A function to apply an optimistic update

Using the same “add item“ example, we can rewrite it using useOptimistic:

function ItemList() {
  const [items, setItems] = useState([]);
  const [optimisticItems, addOptimisticItem] = useOptimistic(items, (prevItems, newItem) => [...prevItems, { ...newItem, isOptimistic: true, id: `temp-${Date.now()}` }]);

  const addItem = async (newItem) => {
    addOptimisticItem(newItem);

    try {
      const response = await fetch('/api/items', { method: 'POST', body: JSON.stringify(newItem) });
      const data = await response.json();
        setItems(prev => prev.map(item => item.id.startsWith("temp-") ? data : item))
    } catch (error) {
        setItems(prev => prev.filter(item => !item.id.startsWith("temp-")))
      console.error("Error adding item:", error);
    }
  };

  // ...
}

This version is significantly cleaner and easier to understand, useOptimistic and handles the optimistic update logic, making the code more concise and less prone to errors. Key improvements include:

  • Simplified Logic: useOptimistic abstracts away the complexities of managing an optimistic state.

  • Declarative Approach: The updateFn clearly defines how the optimistic state should be updated.

  • Reduced Boilerplate: Less code is required to achieve the same functionality.

By using useOptimistic, developers can focus on the core logic of their applications rather than getting bogged down in manual state management, leading to more maintainable and robust code.

useActionState: Simplifying Action State Management

React 19 introduces useActionState, a new hook designed to simplify the management of state related to asynchronous actions, such as form submissions or API calls. This hook streamlines the process of tracking loading states, errors, and the results of these actions, leading to cleaner and more maintainable code.

The Problem With Traditional Action State Management

Managing the state of asynchronous actions manually often leads to verbose and repetitive code. Common patterns involve using multiple state variables to track different aspects of the action:

  • Loading State: A boolean variable to indicate whether the action is in progress.

  • Error State: A variable to store any errors that occur during the action.

  • Result State: A variable to store the result of the action (e.g, the data returned from an API call)

Consider a typical of summiting a form:

import { useState } from 'react';

function MyForm() {
  const [formData, setFormData] = useState({ name: '', email: '' });
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);
  const [result, setResult] = useState(null);

  const handleSubmit = async (e) => {
    e.preventDefault();
    setIsLoading(true);
    setError(null);

    try {
      const response = await fetch('/api/submit', {
        method: 'POST',
        body: JSON.stringify(formData),
      });
      const data = await response.json();
      setResult(data);
    } catch (err) {
      setError(err);
    } finally {
      setIsLoading(false);
    }
  };

  // ...
}

This code snippet, even in its simplified form, demonstrates the boilerplate involved in manually managing the action state. Each action requires managing multiple state variables and updating them within the action handler, which becomes increasingly cumbersome as the application's complexity grows.

Simplifying Action State Management with useActionState

useActionState simplifies this process by encapsulating the state management logic within a single hook. It takes an asynchronous function (the action) as an argument and returns an array containing:

  1. action: A function that triggers the asynchronous action

  2. state: An object containing the action’s state:

    pending: A boolean indication of whether the action is in progress

    error: Any error that occurred during the action
    data: The result of the action

Using useActionState, the previous form example can be written as follows:

import { useActionState, useState } from 'react';

function MyForm() {
  const [formData, setFormData] = useState({ name: '', email: '' });
  const [submit, submitState] = useActionState(async (data) => {
    const response = await fetch('/api/submit', {
      method: 'POST',
      body: JSON.stringify(data),
    });
    return await response.json();
  });

  const handleSubmit = (e) => {
    e.preventDefault();
    submit(formData);
  };

  return (
    <div>
        {submitState.pending && <p>Submitting...</p>}
        {submitState.error && <p style={{color: "red"}}>{submitState.error.message}</p>}
        {submitState.data && <p>Success: {JSON.stringify(submitState.data)}</p>}
        <form onSubmit={handleSubmit}>
            <input type="text" name="name" value={formData.name} onChange={e => setFormData({...formData, name: e.target.value})} />
            <input type="email" name="email" value={formData.email} onChange={e => setFormData({...formData, email: e.target.value})} />
            <button type="submit" disabled={submitState.pending}>Submit</button>
        </form>
    </div>
  );
}

This version is significantly cleaner and more concise, useActionState handles the state management behind the scenes, reducing boilerplate and improving code readability. Key improvements include:

  • Encapsulated State: All action-related state is managed within a single hook, making the component logic cleaner.

  • Reduced Boilerplate: Significantly less code is required to manage loading states, errors, and results.

  • Improved Readability: The code is easier to understand and maintain due to the simplified structure.

By using useActionState, developers can streamline the management of asynchronous actions, resulting in more maintainable and efficient React applications. It promotes cleaner code by abstracting away the repetitive logic of manual state management for actions.

React DOM: useFormStatus — Simplifying Form State Management With Actions

React 19 introduces useFormStatus, a new hook specifically designed to simplify form-state management when used in conjunction with React Server Components and Actions. This hook provides valuable information about the status of a form submission, making it easier to provide feedback to the user and manage the form’s state.

The problem with traditional form state management with actions

Traditionally, managing form submissions with server actions, especially in the context of React Server Components, required manual tracking of loading states and potential errors within the action itself. This could lead to scattered logic and make it difficult to provide a cohesive user experience.

Consider a typical form submission using a server action:

// Server Action (server-side)
async function submitForm(formData) {
  'use server';
  try {
    // Perform server-side logic (e.g., database update)
    await someDatabaseOperation(formData);
    return { success: true };
  } catch (error) {
    return { success: false, error: error.message };
  }
}

// Client Component (client-side)
'use client';
import { useState } from 'react';

function MyForm() {
    const [message, setMessage] = useState(null)
  async function handleSubmit(formData) {
    const result = await submitForm(formData);
    if (result.success) {
        setMessage("Success!")
    } else {
        setMessage(result.error)
    }
  }

  return (
    <form action={handleSubmit}>
      {/* Form inputs */}
        {message && <p>{message}</p>}
      <button type="submit">Submit</button>
    </form>
  );
}

With this approach, it lacks direct integration with the form’s submission process. There is no built-in way to easily access the pending state of the submission directly within the client component.

Simplifying Form State Management With useFormStatus

useFormStatus addresses this limitation by providing direct access to the form’s submission status. It’s designed to be used in the client component that renders a form element associated with a server action. The hook returns an object containing the following properties:

  • pending: A boolean indicating whether the form is currently being submitted.

Using useFormStatus, the previous form example can be significantly improved:

// Server Action (server-side)
async function submitForm(formData) {
  'use server';
  try {
    // Perform server-side logic
    await someDatabaseOperation(formData);
    return { success: true };
  } catch (error) {
    return { success: false, error: error.message };
  }
}

// Client Component (client-side)
'use client';
import { useFormStatus } from 'react-dom';

function MyForm() {
  const { pending } = useFormStatus();

  return (
    <form action={submitForm}>
      {/* Form inputs */}
      <button type="submit" disabled={pending}>
        {pending ? 'Submitting...' : 'Submit'}
      </button>
    </form>
  );
}

This version is much cleaner and more directly integrated with the form’s submission process. Key improvements include:

  • Direct Access To Submission Status: useFormStatus provides a simple way to check if the form is pending submission.

  • Improved User Experience: The pending state can provide immediate feedback to the user, such as disabling the submit button or displaying a loading indicator.

  • Simplified Logic: No need for manual state management within the client component to track the submission status.

useFormStatus streamlines form handling with server actions, especially in React Server Components, leading to better user experiences and cleaner code. It also simplifies providing feedback during form submissions by directly exposing the submission state.

use: A New API for Reading Resources in Render

React 19 introduces use, a new API is designed to simplify reading resources like Promies, Context, or other data sources directly within the render phase of components. This significantly streamlines data fetching and context consumption, making code cleaner and more expressive.

The Problem With Traditional Resource Reading in Render

Traditionally, reading resources during rendering required different approaches depending on the type of resources:

  • Promises (Data Fetching): Components had to manage loading states and error handling using state variables and useEffect or similar lifecycle methods. This often led to verbose code and complex state management.

  • Context: Context values were accessed using useContext, which worked well but could lead to re-renders if the context value changed frequently.

  • Other Resources: Reading other types of resources often requires custom solutions, further increasing complexity.

Consider a typical example of fetching data using useEffect:

import { useState, useEffect } from 'react';

function MyComponent() {
  const [data, setData] = useState(null);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    async function fetchData() {
      try {
        const response = await fetch('/api/data');
        const json = await response.json();
        setData(json);
      } catch (err) {
        setError(err);
      } finally {
        setIsLoading(false);
      }
    }

    fetchData();
  }, []);

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

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

  return <div>Data: {JSON.stringify(data)}</div>;
}

This code illustrates the boilerplate involved in managing asynchronous data fetching. Similarly, accessing context required using useContext which, while simple, could trigger unnecessary re-renders in some scenarios.

Simplifying Resource Reading with use

The use API simplifies these scenarios by providing a single, consistent way to read resources within the render phase. It works with Promise, Context, or other resource types.

  • Promises: When use is called with Promise, React suspends the rendering until the Promise resolves. If the Promise rejects, React throws the error, which can be caught by an error boundary.

  • Context: When use is called with a Context object, it returns the current context values. Unlike useContext, use integrates with Suspense and can prevent unnecessary re-renders.

Using use, the previous data fetching example can be written as follows:

import { use } from 'react';

async function fetchData() {
    const response = await fetch('/api/data')
    return response.json()
}

function MyComponent() {
  const data = use(fetchData());

  return <div>Data: {JSON.stringify(data)}</div>;
}

This version is significantly cleaner and more concise. Key improvements include:

  • Simplified Data Fetching: No more manual state management for loading or error states.

  • Suspense Integration: use works seamlessly with Suspense, allowing for declarative loading states.

  • Consistent API: A single API for reading different types of resources.

  • Improved Performance (With Context): use can prevent unnecessary re-renders when used with Context.

Here is an example with contexts:

import { createContext, use } from 'react';

const ThemeContext = createContext('light');

function ThemedComponent() {
    const theme = use(ThemeContext);
    return <p>The theme is: {theme}</p>
}

function App() {
    return (
        <ThemeContext.Provider value="dark">
            <ThemedComponent />
        </ThemeContext.Provider>
    )
}

By using use, developers significantly simplify resource reading in their React components, leading to cleaner, more maintainable, and more performance code. It provides a more declarative way to handle asynchronous operations or context consumption, improving the overall developer experience.

Ref as a Prop: Simplifying Ref Management in Function Components

React 19 introduces the ability to pass ref as a regular prop to function component. This simplifies ref management, especially when working with reusable components or needing to access DOM elements within deeply nested component structures. It eliminates the need for forwardRef in many common scenarios, leading to cleaner and more concise code.

The Problem with Traditional Ref Management in Function Components

Traditionally, If you needed to access a DOM element inside a function component inside a parent component, you had to use forwardRef. This added a bit of boilerplate and could make the code slightly more complex, especially for simple cases.

Consider a reusable input component:

import React, { forwardRef } from 'react';

const MyInput = forwardRef((props, ref) => (
  <input ref={ref} {...props} />
));

function ParentComponent() {
  const inputRef = React.useRef(null);

  React.useEffect(() => {
    if (inputRef.current) {
      inputRef.current.focus();
    }
  }, []);

  return <MyInput placeholder="Enter text" ref={inputRef} />;
}

In this example, forwardRef is necessary to pass the ref from ParentComponent down to the underlying <input> element in MyInput. While this works, it adds extra code and can be slightly confusing for simpler use cases.

Simplifying Ref Management with Ref as a Prop

React 19 simplifies this by allowing you to pass ref as a regular prop to function components. This means you no longer need forwardRef in many common scenarios.

The previous example can now be written as follows:

import React from 'react';

const MyInput = (props) => (
  <input ref={props.ref} {...props} />
);

function ParentComponent() {
  const inputRef = React.useRef(null);

  React.useEffect(() => {
    if (inputRef.current) {
      inputRef.current.focus();
    }
  }, []);

  return <MyInput placeholder="Enter text" ref={inputRef} />;
}

As you can see, forwardRef is no longer needed. The ref prop is passed directly to the MyInput component and then forwarded to the underlying <input> element.

Important Considerations

  • Explicitly Forwarding the Ref: It’s crucial that the function component explicitly forwards the ref prop to the underlying DOM element or component that needs it. If the ref prop isn't used within the function component, it won't work as expected.

  • Caveats with Class Components: This new feature only applies to function components. You still need forwardRef when working with class components.

  • Use Cases where forwardRef is still needed: If you need to manipulate the ref before passing it down (e.g. adding a custom property to the ref object) you will still need forwardRef.

Benefits of Ref as a Prop

  • Simplified Syntax: Eliminates the need for forwardRef in many common cases, making the code shorter and easier to read.

  • Improved Readability: The code is more straightforward and easier to understand, especially for simple ref usage.

  • Reduced Boilerplate: Reduces the amount of code required to manage refs in function components.

  • More Intuitive API: Makes ref management more consistent with other prop passing mechanisms.

By allowing ref to be passed as a regular prop, React 19 simplifies ref management in function components, leading to cleaner, more concise, and more readable code. This change streamlines a common pattern in React development and contributes to a smoother developer experience.

Cleanup Functions for Refs: Ensuring Proper Cleanup and Resource Management

React 19 provides cleanup functions for refs, addressing potential memory leaks and ensuring proper resource management, especially when working with imperative APIs or external libraries. This enhancement provides a more robust and predictable way to handle resources associated with refs.

The Problem with Traditional Ref Management

Traditionally, ref provides a way to access the underlying DOM element or a component instance. However, there is no built-in mechanism to perform cleanup when the component is unmounted or the ref changed. This could lead to issues in certain scenarios:

  • Memory Leaks: if a ref was used to create subscription event listeners or external resources, these resources might not be released when the component is unmounted, leading to memory leaks.

  • Stale References: If a ref pointed to a DOM element that was removed from the DOM, accessing it could lead to errors.

  • Unpredictable Behavior: Without cleanup, the behavior of components using refs could become unpredictable, especially in complex applications with dynamic rendering.

Consider a scenario where you are integrating with a third-party charting library that requires a DOM element to render a chart:

import { useRef, useEffect } from 'react';
import ChartLibrary from 'external-chart-library';

function MyChart() {
  const chartRef = useRef(null);

  useEffect(() => {
    if (chartRef.current) {
      const chart = new ChartLibrary.Chart(chartRef.current, { /* chart options */ });
      // No cleanup here! Potential memory leak
    }
  }, []);

  return <div ref={chartRef} />;
}

In this example, if the MyChart component unmounts, the chart instance created by the third-party library might not be properly destroyed, leading to memory leaks.

Enhancing Refs with Cleanup Functions

React 19 addresses these issues by allowing you to provide a cleanup function when setting a ref. This cleanup function will be called when the component unmounts or when the ref changes to a different element.

You can now pass a function to the ref callback. This function will be called with the old ref value when it changes or when the component unmounts.

<div ref={(node) => {
    // Set the ref
    myRef.current = node;
    return () => {
        // Cleanup logic here. 'node' is the old value
        if(node) {
            // e.g. node.removeEventListener(...) or destroy external instance
        }
    }
}} />

import { useRef, useEffect } from 'react';
import ChartLibrary from 'external-chart-library';

function MyChart() {
  const chartRef = useRef(null);
    const chartInstance = useRef(null)

  useEffect(() => {
    return () => {
        if(chartInstance.current) {
            chartInstance.current.destroy() // Properly destroy the chart
        }
    }
  }, [])

  return <div ref={(node) => {
      if(node) {
          chartInstance.current = new ChartLibrary.Chart(node, { /* chart options */ });
      }
      return () => {
          if(chartInstance.current) {
              chartInstance.current.destroy()
              chartInstance.current = null
          }
      }
  }} />;
}

Now, when MyChart component unmounts, the cleanup function will be called, destroying the chart instance and preventing a memory leak.

Benefits of Cleanup Functions for Refs

  • Prevent memory leaks: Ensures that resources associated with refs are properly released

  • Avoid stale references: Prevents errors caused by accessing the DOM element that has been removed.

  • Improved resource management: Provides a more robust and predictable way to manage resources.

  • Cleaner code: Makes code that interacts with imperative APIs or external libraries cleaner and easier to reason about.

By adding cleanup functions for refs, React 19 provides a crucial mechanism for ensuring proper resource management and preventing potential issues related to memory leaks and slate references. This enhancement makes React a more robust and reliable platform for building complex applications.

React 18

React 18 was released on March 29, 2022. React provides many exciting features that are out of the box. These are not only enhancing the user experience but also making de’ lives easier. So, here are three main features which are going to be released this time.

A Big Challenge with React Application

Concurrency

Concurrency is a major challenge for heavy React apps. In a React app, concurrency can arise when multiple components render simultaneously, when different parts of the app perform complex tasks concurrently, or when network requests are made concurrently.

Managing concurrently effectively requires careful planning and coordination to ensure that the app’s components and processes work together seamlessly and avoid conflicts or race conditions. Without proper handling of concurrency, a React app may experience performance issues, crashes, or other problems.

Improper setState Usage

Improper use of the setState method in the React app can lead to performance degradation because it can trigger unnecessary re-renders of components. When setState is called, it triggers a re-render of the component and all of its children, which can be computationally expensive if the component tree is large or if the components have expensive render methods.

To avoid unnecessary re-renders and improve performance, it’s important to use setState judiciously and only when necessary and to consider using alternative methods such as useReducer or useState with functional updates to minimize the number of re-renders. Properly managing the use setState can help ensure that your React app remains performant and responsive.

React Concurrent Mode

React Concurrent Mode is a cutting-edge feature designed to transform the way React applications handle rendering, bringing a new level of responsiveness and performance. Unlike the traditional synchronous rendering mode, where React processes updates sequentially, the Concurrent Mode enables the framework to manage multiple tasks simultaneously. This allows React to interrupt and prioritize tasks based on user interactions, ensuring that the UI remains fluid even during intensive computations.

Key Concepts in Concurrent Mode

  1. Time Slicing: Time Slicing is a fundamental feature of Concurrent Mode. It enables React to divide rendering work into small, manageable units that can be spread across multiple frames. This ensures that the main thread remains unblocked, allowing high-prioritized tasks like user interactions or animations to be handled promptly. Time Slicing allows React to work on the UI in segments, pausing to address more urgent tasks before resuming where it left off. This results in a smoother and more responsive user experience.

  2. Suspense for Data Fetching: Suspend is another powerful feature that complements Concurrent Mode. It allows React to “suspend“ the rendering of a component until a specific asynchronous, such as data fetching, is complete. During this waiting period, React can continue rendering other parts of a UI or displaying a fallback component (like a loading spinner) to keep the user informed. When combined with Concurrent Mode, Suspense significantly enhances the user experience, particularly in applications that rely heavily on data asynchronous data fetching.

  3. Interruptible Rendering: Interruptible is a game-changing concept for maintaining a responsive UI. In traditional React, rendering can not be interrupted once it starts, leading to potential UI freezes. Concurrent Mode allows React to interrupt rendering to prioritize higher-priority updates. For instance, if the user interacts with the UI while React is rendering, React can pause the current rendering process, handle the interaction, and then resume rendering. This capability ensures that user interactions are reflected in the UI without delay, leading to a more responsive application.

  4. Selective Hydration: Selective Hydration is an advanced concept within Concurrent Mode, particularly useful in server-side rendering (SSR) scenarios. It allows React to hydrate (i.e, make interactive) only the parts of the page currently visible to the users, deferring less critical sections until they are in view. This prioritization improves the perceived loading time of the application.

For more details, check out this article.

Automatic Batching

Batching is when React groups multiple state updates into a single re-render for better performance.

For example, if you have 2 state updates inside of the same click event, React has always batched these into one re-render. If you run the following code, you will see that every time you click, React only performs only render although you set the state twice.

This is great for performance because it avoids unnecessary re-renders. It also prevents your component from rendering “half-finished“ states where only one state variable was updated, which may cause bugs. This might remind you of how a restaurant waiter doesn’t run to the kitchen when you choose the first dish but waits for you to finish your order.

However, React wasn’t consistent about when it batches updates. For example, if you need to fetch data, and then update the state in the handleClick above, the React would not batch the updates, and perform two independent updates.

This is because React used to batch update only during a browser event (like a click), but here, we are updating the state after the event has already been handled (in the fetch callback).

In automatic batching (after upgrading to React 18), no matter from where the states are originating, it will always be re-rendered once.

What if I don’t want to batch?

In this case, you will have to use flushSync in order to re-render the component.

SSR support for Suspense

This is basically an extension of Server Side Rendering (SSR) logic. In a typical React SSR application, the following steps happen:

  • The server fetches the relevant data which needs to be shown on the UI.

  • The server renders the entire app to HTML and sends it to the client in response.

  • The client downloads the Javascript bundle (apart from HTML).

  • In the final step, the client connects the Javascript logic to the HTML (which is known as hydration)

The problem with the typical SSR application is that each step had to finish the entire app at once before the next step could start. This makes your app a bit slower and unresponsive at initial load time.

React 18 enhances SSR by allowing Suspense to work with streaming rendering:

  • Instead of waiting for the entire server-rendered HTML to generate, React streams the HTML in parts, rendering each Suspense boundary as soon as its data is ready.

  • This approach allows users to see some content earlier, improving perceived performance.

// Server-side
renderToPipeableStream(<App />, {
  onShellReady() {
    stream.pipe(response);
  },
});

Here, <Suspense/> ensures parts of the UI are progressively sent to the client while the remaining parts continue loading.

Transition

This is an incredible feature going to be released. It lets users resolve the issue of frequent updates on large screens. For example, consider typing in an input field that filters a list of data. You need to store the value of the field in the state so you can filter the data and control the value of that input field. Your code may look like something like this:

// Update the input value and search results
setSearchQuery(input);

Here, whenever the user types a character, we update the input value and use the new value to search the list and show the results. For large-screen updates, this can cause lag on the page while everything renders, making typing or other interactions feel slow and unresponsive. Even if the list is not too long, the list items themselves may be complex and different with every keystroke, and there may be no clear way to optimize their rendering.

Conceptually, this issue involves two different updates that need to happen. The first is urgent, changing the value of the input field and potentially some UI around it. The second is less urgent, showing the search results.

// Urgent: Show what was typed
setInputValue(input);

// Not urgent: Show the results
setSearchQuery(input);

The new startTransition API solves this issue by giving you the ability to mark updates as “transitions“:

import { startTransition } from 'react';
// Urgent: Show what was typed
setInputValue(input);
// Mark any state updates inside as transitions
startTransition(() => {
  // Transition: Show the results
  setSearchQuery(input);
});

5 New Hooks in React 18

useTransition

useTransition() is a hook for transition. It returns the transition state and a function to start the transition:

const [isPending, startTransition] = useTransition();

React state updates are classified into two categories:

  • Urgent updates: They reflect direct interaction such as typing, clicking, pressing, dragging, etc.

  • Transition updates: They transition the UI from one view to another.

import { useEffect, useState, useTransition } from 'react';

const SlowUI = ({ value }) => (
  <>
    {Array(value)
      .fill(1)
      .map((_, index) => (
        <span key={index}>{value - index} </span>
      ))}
  </>
);

function App() {
  const [value, setValue] = useState(0);
  const [value2, setValue2] = useState(100000);
  const [isPending, startTransition] = useTransition();

  const handleClick = () => {
    setValue(value + 1);
    startTransition(() => setValue2(value2 + 1));
  };

  return (
    <>
      <button onClick={handleClick}>{value}</button>
      <div
        style={{
          opacity: isPending ? 0.5 : 1,
        }}
      >
        <SlowUI value={value2} />
      </div>
    </>
  );
}

export default App;

The above application is composed of two components (lines 25–32):

  • button (line 25): It is a simple button. The display number is controlled by value at line 14. Clicking the button increases value (line 19, urgent and fast update) and value2 (line 20, transition and slow update).

  • SlowUI (lines 26–32): The component is defined in lines 3–11, which generates 100000+ span elements controlled by value2 at line 15. It takes a longer time to update so many elements. useTransition at line 16 returns the transition state, isPending, and the function to start the transition, startTransition. When startTransition is called on line 20, isPending turns true, and SlowUI is half opaque (light colored) with stale data (line 28). When the transition finishes, isPending turns false, and SlowUI becomes fully opaque (solid colored) with updated data.

Try to remove startTransition line 20 to call setValue2(value2 + 1) directly. You can see that the UI is no longer functional with so many updates happening simultaneously.

The useTransition hook returns isPending and startTransition. If you do not need to show special UI for isPending, remove line 16, and add the following line at the top of the code.

import { startTransition } from 'react';

useDeferredValue

useDeferredValue(value) is a hook that accepts a value and returns a new copy of the value that will defer to more urgent updates. The previous value is kept until urgent updates have been completed. Then, the new value is rendered. This hook is similar to using debouncing or throttling to defer updates.

import { useDeferredValue, useState } from 'react';

const SlowUI = () => (
  <>
    {Array(50000)
      .fill(1)
      .map((_, index) => (
        <span key={index}>{100000} </span>
      ))}
  </>
);

function App() {
  const [value, setValue] = useState(0);
  const deferredValue = useDeferredValue(value);

  const handleClick = () => {
    setValue(value + 1);
  };

  return (
    <>
      <button onClick={handleClick}>{value}</button>
      <div>DeferredValue: {deferredValue}</div>
      <div>
        <SlowUI />
      </div>
    </>
  );
}

export default App;

The above application is composed of three components (lines 23–27):

  • button (line 23): It is a simple button. The display number is controlled by value at line 14. Clicking the button increases value (line 18, urgent and fast update).

  • div (line 24): It displays deferredValue.

  • SlowUI (lines 25–27): The component is defined at lines 3–11, which generates 50000 fixed-number span elements. Although the component does not have props and visually does not update, it takes a long time to update so many elements.

useDeferredValue can be used in conjunction with startTransition and useTransition.

useId

In a Web application, there are cases that need unique IDs, for example:

  • <label for="ID">, where the for attribute must be equal to the id attribute of the related element to bind them together.

  • aria-labelledby, where the aria-labelledby attribute could take multiple IDs.

useId() is a hook that generates a unique ID:

  • This ID is stable across the server and the client, which avoids hydration mismatches for server-side rendering.

  • This ID is unique for the entire application. In the case of multi-root applications, createRoot/hydrateRoot has an optional prop, identifierPrefix, which can be used to add a prefix to prevent collisions.

  • This ID can be appended with prefixes and/or suffixes to generate multiple unique ids that are used in a component. It seems trivial. But, useId was evolved from useOpaqueIdentifier, which generates an opaque ID that cannot be operated upon.

import { useId } from 'react';

const Comp1 = () => {
  const id = useId();
  return <div>Comp1 id({id})</div>;
};

const Comp2 = () => {
  const id = useId();
  return (
    <>
      <div>Comp2 id({id})</div>
      <label htmlFor={`${id}-1`}>Label 1</label>
      <div>
        <input id={`${id}-1`} type="text" />
      </div>
      <label htmlFor={`${id}-2`}>Label 2</label>
      <div>
        <input id={`${id}-2`} type="text" />
      </div>
    </>
  );
};

const Comp3 = () => {
  const id = useId();
  return (
    <>
      <div>Comp3 id({id})</div>
      <div aria-labelledby={`${id}-a ${id}-b ${id}-c`}>I am Comp3</div>
    </>
  );
};

function App() {
  return (
    <>
      <Comp1 />
      <Comp2 />
      <Comp3 />
    </>
  );
}

export default App;

The above application is composed of three components (lines 38–40):

  • Comp1: It is defined in lines 3–6, which generates and displays one id, :r0:.

  • Comp2: It is defined in lines 8–23, which generates one id, :r1:. From this one id, it derives two unique IDs, :r1:-1 (for Label 1 + the input field) and :r1:-2 (for Label 2 + the input field).

  • Comp3: It is defined in lines 25–33, which generates and displays one id, :r2:. From his one ID, it derives three unique IDs, :r1:-a, :r1:-b, and :r1:-c, for the aria-labelledby attribute.

Execute the code by npm start. We see the following UI, along with the generated HTML elements in Chrome DevTools.

useSyncExternalStore

useSyncExternalStore is a hook recommended for reading and subscribing to external sources (stores):

Here is the signature of the hook:

const state = useSyncExternalStore(subscribe, getSnapshot[, getServerSnapshot]);

This method accepts three arguments:

  • subscribe: It’s a function to register a callback that is called whenever the store changes.

  • getSnapshot: This is a function that returns the current value of the store.

  • getServerSnapshot: It is the function that returns the snapshot used during server rendering. This is an optional parameter.

This method returns the value of the store, state.

We created an example of useSyncExternalStore, which reads the browser window width and displays it on the screen.

import { useSyncExternalStore } from 'react';

function App() {
  const width = useSyncExternalStore(
    (listener) => {
      window.addEventListener('resize', listener);
      return () => {
        window.removeEventListener('resize', listener);
      };
    },
    () => window.innerWidth
    // () => -1,
  );

  return <p>Size: {width}</p>;
}

export default App;

The above application calls useSyncExternalStore:

  • subscribe (lines 5–10): It registers a callback for the window resize event listener.

  • getSnapshot (line 11): It returns the current browser window width.

  • getServerSnapshot (line 12): It is for server rendering, which is not needed here, or simply returns -1.

Execute the code by npm start. The following video shows that the UI displays the browser window width while resizing.

useInsertionEffect

useEffect(didUpdate) accepts a function that contains imperative, possibly effective code, which are mutations, subscriptions, timers, logging, and other side effects. By default, effects run after every completed render, but the invocation can be controlled with an array of second arguments.

useLayoutEffect has the same signature as useEffect, but it fires synchronously after all DOM mutations. it’s fired before useEffect. It’s used to read the layout from the DOM and asynchronously re-render. Updates scheduled inside useLayoutEffect will be flushed synchronously before the browser has a chance to paint.

useInsertionEffect is introduced in React 18. It has the same signature as useEffect, but it fires synchronously before all DOM mutations. i.e. it is fired before useLayoutEffect. It is used to inject styles into the DOM before reading the layout.

useInsertionEffect is intended for CSS-and-Js libraries, such as styled-components. Since this hook is limited in scope, this hook does not have access to refs and cannot schedule updates.

The following example, placed in src/App.js, compares useEffect, useLayoutEffect, and useInsertionEffect:

import { useEffect, useInsertionEffect, useLayoutEffect } from 'react';

const Child = () => {
  useEffect(() => {
    console.log('useEffect child is called');
  });
  useLayoutEffect(() => {
    console.log('useLayoutEffect child is called');
  });
  useInsertionEffect(() => {
    console.log('useInsertionEffect child is called');
  });
};

function App() {
  useEffect(() => {
    console.log('useEffect app is called');
  });
  useLayoutEffect(() => {
    console.log('useLayoutEffect app is called');
  });
  useInsertionEffect(() => {
    console.log('useInsertionEffect app is called');
  });
  return (
    <div className="App">
      <Child />
      <p>Random Text</p>
    </div>
  );
}

export default App;

The above application has an App (lines 15–31) and a Child component (lines 3–13). Both of them call useEffect, useLayoutEffect, and useInsertionEffect.

Execute the code by npm start, and we see the displaying text and console output.

These effects are called in the following order:

  • useInsertionEffect child is called.

  • useInsertionEffect app is called.

  • useLayoutEffect child is called.

  • useLayoutEffect app is called.

  • useEffect child is called.

  • useEffect app is called.

This is the expected order.

But, why are they called twice?

With the release of React 18, StrictMode gets an additional behavior that is called strict effects mode. When strict effects are enabled, React intentionally double-invokes effects (mount -> unmount -> mount) for newly mounted components in development mode. Interestingly, useInsertionEffect is not called twice.

Execute npm run build, and serve -s build. We can verify that in production mode, this app is only mounted once.

Let’s go back to the development mode. After npm start, modify the text to Random Text is changed, and save src/App.js. The additional console log shows how these effects are called upon changes.

useInsertionEffect is the hook to be used if we want it to fire before all DOM mutations. However, useInsertionEffect is intended for CSS-in-JS libraries. For normal application developers, useEffect or useLayoutEffect are used more commonly.

React 17

Over two years after React 16 was launched, the long-awaited React 17 was released in October 2020. While the update introduced numerous great changes as always, this release is unique because it contains no new developer-facing features. While React 16 added significant features like Hooks and the Context API, React 17 is mostly considered a stepping stone to ease the transition for feature releases.

New JSX Enhancements

A superficial examination of React 17 is sure to leave you unimpressed. What is fascinating is not the new features but the way React is compiled.

To understand these better, let’s examine the compiled code of JSX in a component that uses an older version of React.

You might notice that the complied version uses React.createElement, where React dependency should be available in the scope. That’s why you need to import React in the first place in each of the components.

Now let’s take a look at how it works with React 17

With React 17, you don’t need the React import for JSX.

I hope that gives you a clue that the compiled version doesn’t require the React import. As you can see in the following image, the React 17 compiler imports a new dependency from react/jsx-runtime, which handles the JSX transformation.

So as developers, once you upgrade to React 17, you can remove the React import from your component’s code if it’s only there for JSX.

With the removal of React import, your compiled bundled output will become slightly smaller. I hope it becomes evident since we need to remove the React import from each of the components where the compiler replaces it with a submodule in React, as shown below:

import {jsx as _jsx} from 'react/jsx-runtime';

Event Delegation

In React (version < React 17), whenever we write any event handlers on elements, it doesn't attach events to the specific DOM nodes, instead, it attaches each handler to the document node. This is called event delegation.

To enable gradual updates, this is the problem if you have multiple React versions on the same page and they all register handlers at the document level.

To fix this, in React 17, all handlers will be attached to the root DOM container defined in your React app where you render the root App component.

React 17 will no longer attach event handlers at the document level. Instead, it will attach them to the root DOM container into which your React tree is rendered.

Effect Cleanup Timing

In React 16, the cleanup function timing in useEffect was synchronous, which meant that when the component was in the unmounting phase, the cleanup function would run first and then the screen would get updated. If the cleanup logic was computationally expensive or involved asynchronous operations, such as subscribing from an event listener or canceling a network request, it could delay the screen update. This delay was noticeable in heavy apps where multiple components with cleanup logic were being unmounted simultaneously.

Users would perceive a lag in UI responsiveness during transitions or component unmounts. For example, removing a modal or switching views might take longer because the cleanup function execution blocked the screen update.

React 17 fixes this by making the cleanup function asynchronous. This improves performance.

According to the React blog, “In React v17, the effect cleanup function always runs asynchronously - for example, if the component is unmounting, the cleanup runs after the screen has been updated.”

React 16.0 - 16.8

React 16 had 14 sub-releases from 16.0.0 to 16.14.0. Each release introduced improvements, bug fixes, and new features while maintaining backward compatibility.

Returns multiple elements from components with fragments

Splitting UI into multiple reusable small components may lead to creation of unnecessary DOM elements, like when you need to return multiple elements from a component. React 16 has several options to avoid that:

// React 15: extra wrapper element
const Breakfast = () => (
  <ul>
    <li>Coffee</li>
    <li>Croissant</li>
    <li>Marmalade</li>
  </ul>
);

// React 16.0: array (note that keys are required)
const Breakfast = () => [
  <li key="coffee">Coffee</li>,
  <li key="croissant">Croissant</li>,
  <li key="marmalade">Marmalade</li>
];

// React 16.2: fragment
const Breakfast = () => (
  <React.Fragment>
    <li>Coffee</li>
    <li>Croissant</li>
    <li>Marmalade</li>
  </React.Fragment>
);

// React 16.2: fragment (short syntax)
const Breakfast = () => (
  <>
    <li>Coffee</li>
    <li>Croissant</li>
    <li>Marmalade</li>
  </>
);

// React 16: fragments composition
const Meals = (
  <ul>
    <Breakfast />
    <Lunch />
    <Dinner />
  </ul>
);

Note that the short syntax may not be supported by the tools you are using.

Returning strings and numbers from components

React 16 components can return strings and numbers. This is useful for components that don’t need any markup, like internationalization or formatting:

// React 15
const LocalDate = ({ date }) => (
  <span>
    {date.toLocaleDateString('de-DE', {
      year: 'numeric',
      month: 'long',
      day: 'numeric'
    })}
  </span>
);

// React 16
const LocalDate = ({ date }) =>
  date.toLocaleDateString('de-DE', {
    year: 'numeric',
    month: 'long',
    day: 'numeric'
  });

Cancelling setState() to avoid rendering

In React 15, It wasn’t possible to cancel setState() and avoid rendering, if your next state was based on the previous state. In React 16 you could return null in setState()’s callback.

// React 16
handleChange = event => {
  const city = event.target.value;
  this.setState(
    prevState => (prevState.city !== city ? { city } : null)
  );
};

In this example calling handleChange() with the same city name as in the state won’t cause a rerender.

Avoid prop drilling with the official Context API (16.3)

Prop drilling is when you pass some data to a deeply nested component using a prop. You have to add this prop to each layer of your React component tree between a component that owns the data and a component that consumes it.

class Root extends React.Component {
  state = { theme: THEME_DARK };
  handleThemeToggle = theme =>
    this.setState(({ theme }) => ({
      theme: theme === THEME_DARK ? THEME_LIGHT : THEME_DARK;
    }));
  render() {
    return (
      <Page
        onThemeToggle={this.handleThemeToggle}
        {...this.state}
        {...this.props}
      />
    );
  }
}
// Each layer will have to pass theme and theme toggle handler props
<SomeOtherComponent
  onThemeToggle={props.onThemeToggle}
  theme={props.theme}
/>;
// Many layers below
const Header = ({ theme, onThemeToggle }) => (
  <header className={cx('header', `header--${theme}`)}>
    ...
    <button onClick={onThemeToggle}>Toggle theme</button>
  </header>
);

That is a lot of boilerplate code! With the Context API, we can access our theme props anywhere in the component tree.

const ThemeContext = React.createContext(THEME_DARK);
// We should wrap our app in this component
class ThemeProvider extends React.Component {
  state = { theme: THEME_DARK };
  handleThemeToggle = theme =>
    this.setState(({ theme }) => ({
      theme: theme === THEME_DARK ? THEME_LIGHT : THEME_DARK
    }));
  render() {
    return (
      <ThemeContext.Provider
        value={{
          onThemeToggle: this.handleThemeToggle,
          theme: this.state.theme
        }}
      >
        {this.props.children}
      </ThemeContext.Provider>
    );
  }
}
// And then use theme consumer anywhere in the component tree
const Header = () => (
  <ThemeContext.Consumer>
    {({ theme, onThemeToggle }) => (
      <header className={cx('header', `header--${theme}`)}>
        ...
        <button onClick={onThemeToggle}>Toggle theme</button>
      </header>
    )}
  </ThemeContext.Consumer>
);

Updating state based on props with getDerivedStateFromProps()

The getDerivedStateFromProps() lifecycle method is a replacement for componentWillReceiveProps(). It’s useful when you have a prop with a default value for a state property, but you want to reset the state when that prop changes. For example, a modal that has a prop that says if it’s initially open, and a state that says if a modal is open now.

// React 15
class Modal extends React.Component {
  state = {
    isOpen: this.props.isOpen
  };
  componentWillReceiveProps(nextProps) {
    if (nextProps.isOpen !== this.state.isOpen) {
      this.setState({
        isOpen: nextProps.isOpen
      });
    }
  }
}

// React 16.3
class Modal extends React.Component {
  state = {};
  static getDerivedStateFromProps(nextProps, prevState) {
    if (nextProps.isOpen !== prevState.isOpen) {
      return {
        isOpen: nextProps.isOpen
      };
    }
  }
}

The getDerivedStateFromProps() method is called when a component is created and when it receives new props, so you don’t have to convert props to state twice (on initialization and in componentWillReceiveProps()).

Rendering function components on props change with React.memo() (16.6)

React.memo() does the same for function components as PureComponent does for class components: only the rerendered component if its props change.

const MyComponent = React.memo(props => {
  /* Only rerenders if props change */
});

Other new features

Conclusion

By staying up-to-date with these new methods, developers can leverage the full potential of React.js, writing cleaner and more maintainable code. These advancements reflect React’s commitment to improving developer productivity and delivering high-quality user experiences. Whether you are building a complex web application or a simple component, incorporating these new methods into your workflow can lead to significant improvements in both performance and useability.

References

https://medium.com/gradeup/upgrading-to-react-17-5a5aad38057f

https://levelup.gitconnected.com/react-19-released-new-game-changing-features-for-developers-you-must-know-50535f8e05f8

https://blog.bitsrc.io/unleash-the-power-of-server-side-rendering-with-react-server-components-and-next-js-13-10448b803611

https://levelup.gitconnected.com/react-19-released-new-game-changing-features-for-developers-you-must-know-50535f8e05f8

https://javascript.plainenglish.io/what-you-need-to-know-about-react-18-54070f6bc4a1

https://betterprogramming.pub/5-new-hooks-in-react-18-300aa713cefe

https://levelup.gitconnected.com/react-18-optimizing-performance-with-async-rendering-and-concurrent-mode-60f8c5957f3

https://blog.bitsrc.io/new-jsx-enhancements-in-react-17-e5f64acbea89

https://medium.com/hackernoon/react-16-0-16-3-new-features-for-every-day-use-f397da374acf

0
Subscribe to my newsletter

Read articles from Tuan Tran Van directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Tuan Tran Van
Tuan Tran Van

I am a developer creating open-source projects and writing about web development, side projects, and productivity.