A Deep Dive Into Hooks In React 18

OpenReplay Tech Blog - Jun 8 - - Dev Community

by Chidi Confidence

In the dynamic realm of web development, React 18 emerges as a beacon of innovation, introducing a plethora of features designed to empower developers and enhance the user experience. Among the noteworthy additions are four powerful hooks: `useTransition`, `useMutableSource`, `useDeferredValue`, and `useSyncExternalStore`. This comprehensive exploration delves into the intricacies of each hook, providing developers with a profound understanding of their functionality and usage.

Session Replay for Developers

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — an open-source session replay suite for developers. It can be self-hosted in minutes, giving you complete control over your customer data.

OpenReplay

Happy debugging! Try using OpenReplay today.


React's Version 18 represents a significant evolution in the React ecosystem, bringing forth improvements in performance, rendering, and state management. It introduces features that simplify development workflows and pave the way for novel approaches to building user interfaces. The release focuses on equipping developers with tools to create more responsive, efficient, and dynamic web applications.

Benefits of Version 18 Hooks

Explore the following unique benefits for elevated development experiences:

  • useTransition: Enhances the creation of smooth and responsive user interfaces by facilitating graceful transitions between different states. This hook offers developers control over when should commit the transition, empowering them to prioritize rendering or user interactions based on their specific needs.
  • useMutableSource: Facilitates seamless integration with external data sources, empowering to interact with mutable data without triggering unnecessary re-renders. This is particularly valuable in scenarios where the data source can be mutated without necessitating a complete re-render of the component tree.
  • useDeferredValue: Optimizes performance by deferring certain updates until the browser is idle. It is beneficial in scenarios where specific updates are not time-sensitive, allowing them to be delayed to enhance the overall user experience.
  • useSyncExternalStore: Streamlines synchronization between React state and external state management systems. It proves valuable in scenarios where external stores, such as React-Redux or MobX, require alignment with internal state for seamless coordination.

useTransition

The useTransition hook is a relatively new addition to the collection of hooks introduced in version 18. It is designed to manage complex UI transitions by allowing you to defer the rendering of certain components. Doing so helps prevent performance bottlenecks and improves the user experience during intense updates or transitions.

The primary goal of useTransition is to enable applications to stay responsive and maintain smooth animations even during heavy operations, such as data fetching, large-scale updates, or any process that might cause the UI to freeze temporarily.

Follow these steps to start using useTransition in your react application:

  • Update to version 18 (or higher): Before using the useTransition hook, ensure that your application is running on version 18 or higher. This hook was introduced in React 18, so you need to update your project’s dependencies accordingly.
  • Import the hook: To use the useTransition hook, import it from the package.
import { useTransition } from 'react';
Enter fullscreen mode Exit fullscreen mode
  • Define the Transition: Next, define the transition using the useTransition hook. The hook requires two arguments: the resource-intensive function and a configuration object.
const [startTransition, isPending] = useTransition();
Enter fullscreen mode Exit fullscreen mode

Example.

import { useState, useTransition } from "react";

export default function App() {
  const [data, setData] = useState([]);
  const [isPending, startTransition] = useTransition({ timeoutMs: 3000 });

  const fetchData = async () => {
    startTransition(() => {
      setData([]);
    });

    try {
      const response = await fetch(
        "https://jsonplaceholder.typicode.com/todos"
      );
      const data = await response.json();
      startTransition(() => {
        setData(data);
      });
    } catch (error) {
      console.error(error);
    }
  };

  return (
    <div>
      <button onClick={fetchData}> Fetch Data </button>
      {isPending ? (
        <p> Loading... </p>
      ) : (
        <ul>
          {data.map((item) => (
            <li key={item.id}>{item.title}</li>
          ))}
        </ul>
      )}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

In this example, we leverage the power of the useTransition hook in a component to orchestrate a smooth and responsive transition between different states while fetching data from an external API. The primary goal is to enhance the user experience by providing feedback during the asynchronous operation and ensuring a seamless transition between loading and displaying the fetched data. When the user clicks the "Fetch Data" button, the fetchData function is triggered. Inside this function, we initiate the first transition using startTransition. During this transition, the setData function is called to update the data state to an empty array, and the isPending state is set to true. As a result, a loading indicator is displayed, signaling to the user that data is being fetched. Simultaneously, the actual asynchronous API call is made using the fetch function. Upon successful retrieval of the API response, a second transition is triggered. This time, the setData function updates the data state with the fetched data, and the isPending state is set to false. Consequently, the loading indicator disappears, and the fetched data is rendered in the component.

It's important to note that the use of useTransition helps optimize the user interface by managing the timing and sequencing of state transitions. The provided timeoutMs option specifies a maximum duration for the transition, ensuring that the user interface remains responsive even if the API call takes some time to complete. Overall, this approach contributes to a more performant and user-friendly application. Run code

Output.

chrome_5gEP94EcYE

useMutableSource

The useMutableSource hook enhances the integration of external data sources or libraries within React's rendering pipeline by efficiently managing mutable data sources. This results in improved performance and facilitates smoother updates to components.

Example.

import { useMutableSource } from 'react';

function App() {
  const mutableSource = getMutableSource(); // Get the mutable source from an external library
  const data = useMutableSource(mutableSource, getData, subscribe);

  return <div>{data}</div>;
}
Enter fullscreen mode Exit fullscreen mode

In this illustration, the getMutableSource() function is employed to acquire the mutable source from an external library. The getData function fetches data from this mutable source, while the subscribe function registers a callback to receive data updates. The useMutableSource hook adeptly handles the subscription process and ensures that the data variable is promptly updated whenever changes transpire in the mutable source.

useDeferredValue

The useDeferredValue hook allows developers to defer the processing of expensive computations until later. This can help improve your application's performance by reducing the amount of work that needs to be done during each render cycle.

Example.

import { useState, useEffect, useDeferredValue } from "react";

const AsyncOperation = () => {
  const [data, setData] = useState([]);
  const [page, setPage] = useState(1);

  const deferredPage = useDeferredValue(page, { timeoutMs: 1000 });

  const fetchData = async () => {
    try {
      const response = await fetch(
        `https://jsonplaceholder.typicode.com/photos?_page=${deferredPage}`
      );

      const newData = await response.json();
      setData((prevData) => [...prevData, ...newData]);
    } catch (error) {
      console.error(error);
    }
  };

  useEffect(() => {
    fetchData();
  }, [deferredPage]);

  const handleLoadMore = () => {
    setPage((prevPage) => prevPage + 1);
  };

  return (
    <div>
      <button onClick={handleLoadMore}> Load More </button>
      <ul>
        {data.map((item) => (
          <li key={item.id}>
            <img src={item.thumbnailUrl} alt={item.title} width={20} />
          </li>
        ))}
      </ul>
    </div>
  );
};

export default AsyncOperation;
Enter fullscreen mode Exit fullscreen mode

Output.

chrome_q80dOu9VsA

In this example, the AsyncOperation component utilizes the useDeferredValue hook to optimize the rendering and performance of an asynchronous data fetching operation. The primary goal is to defer the update of the page state by one second when the user interacts with the "Load More" button. This delay, facilitated by the useDeferredValue hook, optimizes the rendering process and prevents unnecessary re-renders triggered by rapid state changes. Let's break down the key aspects of the code.

Defer State with useDeferredValue.

const deferredPage = useDeferredValue(page, { timeoutMs: 1000 });
Enter fullscreen mode Exit fullscreen mode

The useDeferredValue hook creates a deferred version of the page state. This means that when the user clicks the "Load More" button, the immediate update of the page state is delayed by one second. During this delay, the deferredPage variable reflects the current state of the page but with a one-second lag.

Fetch Data Function.

const fetchData = async () => {
  try {
    const response = await fetch(
      `https://jsonplaceholder.typicode.com/photos?_page=${deferredPage}`
    );

    const newData = await response.json();
    setData((prevData) => [...prevData, ...newData]);
  } catch (error) {
    console.error(error);
  }
};
Enter fullscreen mode Exit fullscreen mode

The fetchData function makes an asynchronous API call to fetch data based on the current deferredPage. This function is invoked within the useEffect hook, ensuring it runs whenever the deferred page changes.

useEffect Hook for Fetching Data.

useEffect(() => {
  fetchData();
}, [deferredPage]);
Enter fullscreen mode Exit fullscreen mode

The useEffect hook is set up to execute the fetchData function whenever the deferredPage changes. Since the deferredPage value lags behind the actual page state by one second, React has a brief period to optimize rendering and avoid unnecessary updates triggered by frequent state changes.

Load More Button Handler.

const handleLoadMore = () => {
  setPage((prevPage) => prevPage + 1);
};
Enter fullscreen mode Exit fullscreen mode

The "Load More" button click event triggers the handleLoadMore function, which increments the page state. Due to the deferred nature of deferredPage, React introduces a one-second delay before updating deferredPage to the new page value.

The utilization of the useDeferredValue hook in this code exemplifies a strategy to enhance the performance of components, particularly in scenarios involving user interactions leading to frequent state updates. The one-second delay introduced by useDeferredValue allows for strategic management rendering and ensures a more efficient handling of asynchronous data fetching operations. Run code

useSyncExternalStore

The useSyncExternalStore hook empowers developers to harmonize a component's state with an external store. This functionality is valuable in crafting sophisticated applications that amalgamate data from diverse sources.

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

const MyComponent = () => {
  const [count, setCount] = useState(0);
  const syncExternalStore = useSyncExternalStore();

  useEffect(() => {
    // fetch the initial count from an external API
    fetch('https://example.com/count')
      .then(response => response.json())
      .then(data => setCount(data.count))
      .catch(error => console.error(error));
  }, []);

  useEffect(() => {
    // sync the count value with the external store
    syncExternalStore('count', count);
  }, [count, syncExternalStore]);

  const incrementCount = () => {
    setCount(count + 1);
  };

  return (
    <div>
      <div> Count: {count} </div>
      <button onClick={incrementCount}> Increment Count </button>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

In this illustrative example, we employ the useSyncExternalStore hook to ensure synchronization of a count value with an external data store. To begin, we establish a count state variable via the useState hook, initializing it to 0. Subsequently, we employ the useEffect hook to retrieve the initial count value from an external API, updating the count state variable through the setCount method. We utilize another useEffect hook to synchronize the count value with the external store, employing the syncExternalStore method. This method requires two arguments: a key value identifying the data to be synchronized (here, 'count') and the current value of the data. Lastly, we define aincrementCount function responsible for incrementing the count state variable by 1. We then render a button that invokes this function upon clicking.

By leveraging useSyncExternalStore, we maintain coherence between our application state and external data stores, such as databases or caching systems. This proves particularly beneficial in the development of real-time applications or collaborative platforms requiring shared state across multiple devices or users.

Conclusion

In summary, version 18 of React introduces a diverse range of new hooks, empowering developers to construct applications that are not only more efficient but also more accessible. By harnessing these innovative hooks, developers can craft applications with enhanced flexibility and performance, all while minimizing the necessity for redundant boilerplate code. These hooks constitute a valuable enhancement to the library, destined to become indispensable tools for developers immersed in React development.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .