Crucial React Hooks That Are Less Understood

Abeer Abdul AhadAbeer Abdul Ahad
12 min read

Introduction

As React has become the top choice for building web applications, it's important to master all the concepts and topics that makes learning react incomplete without. If you have ever studied react for at least once, I am sure you know that react have a list of built-in hooks which are crucial for working with react and I am sure you know useState and useEffect among them pretty well. That's why I am assuming with confidence that I don't need to touch them. Instead, I'll help you master the hooks that are often less understood:

  1. useLayoutEffect

  2. useMemo

  3. useCallback

  4. useReducer

  5. useRef

  6. useImperativeHandle

  7. useDeferredValue

  8. useTransition

These are very important if you want to be comfortable to work on any feature with react which you should as a react developer.

useLayoutEffect

Before getting into useLayoutEffect we need to talk about useEffect. How does useEffect hook work? Imagine where the component is rendered is a canvas for you to paint on. So, you visualize the picture on your head first and then paint it on the canvas. Now you want to add some extra details after painting is done. In this task, adding those extra details right after you complete painting is useEffect hook. On the other hand, adding those extra details before actual painting is useLayoutEffect hook.

Visualization on head > rendering the component DOM.

Adding Extra details > Side effect or the code written inside useEffect or useLayoutEffect hook.

Painting picture > painting the web page UI on the browser window.

Not clear yet? See if you understand the following explanation:

  • useEffect runs after the screen updates, which is good for non-urgent tasks and won't delay what the user sees.

  • useLayoutEffect runs after the DOM is rendered and before the screen updates, which is good for urgent tasks that need to happen right away but can delay what the user sees if it takes too long.

For useLayoutEffect hook it doesn't have to work on the first render. It works the same for all the re-renders as well.

import React, { useLayoutEffect, useRef } from 'react';

function MyComponent() {
  const ref = useRef(null);

  useLayoutEffect(() => {
    // This code will run after the render but before the browser paints
    if (ref.current) {
      console.log(ref.current.getBoundingClientRect());
    }
  }, []);

  return <div ref={ref}>Hello, World!</div>;
}

How useLayoutEffect can hurt performance

useLayoutEffect can hurt performance because it runs synchronously after all DOM mutations but before the browser has a chance to paint. This synchronous behavior means that useLayoutEffect can block the browser's painting process, causing the following potential performance issues:

  1. Blocking the Browser's Render: Since the browser needs to wait for the synchronous effect to complete before painting

  2. Heavy Computations: If you perform heavy computations or complex operations inside useLayoutEffect, these operations will block the main thread. As a result, the browser cannot perform other critical tasks like handling user interactions or rendering other parts of the UI, leading to a sluggish user experience.

  3. Frequent Updates: If useLayoutEffect is used in a component that updates frequently, each update will involve running the synchronous effect, which can repeatedly block rendering

useMemo

useMemo helps you optimize your components by memoizing (caching) the results of expensive calculations. This means React will remember the result of a calculation and reuse it until the inputs change, rather than recalculating it every time the component renders.

When to UseuseMemo:

  • Expensive Calculations: Use useMemo for calculations that take a lot of time or resources, to avoid recalculating them on every render.

  • Referential Equality: Helps to prevent unnecessary re-renders of child components by ensuring that the reference to the value remains the same unless the dependencies change.

How it works: When you wrap the expensive calculation inside useMemo and give a list of dependencies, React will only recalculate the result when one of the dependencies changes.

Simple Example: Imagine you have a list of numbers and you want to calculate the sum. This sum calculation is expensive, and you don’t want to do it on every render unless the list of numbers changes.

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

function SumCalculator({ numbers }) {
  const sum = useMemo(() => {
    console.log('Calculating sum...');
    return numbers.reduce((acc, num) => acc + num, 0);
  }, [numbers]);

  return <div>The sum is: {sum}</div>;
}

export default function App() {
  const [numbers, setNumbers] = useState([1, 2, 3, 4]);

  return (
    <div>
      <SumCalculator numbers={numbers} />
      <button onClick={() => setNumbers([...numbers, numbers.length + 1])}>
        Add Number
      </button>
    </div>
  );
}

useCallback

Unlike useMemo(), useCallback() hook memoizes(caches) functions. This means React will remember the function and reuse it until its dependencies change, rather than recreating it on every render. As we know, functions in a component get recreated when a re-render happens.

useCallback()use cases:

  • When you pass functions as props to child components, using useCallback ensures that the function reference remains the same unless dependencies change. This prevents unnecessary re-renders of the child components.

  • using useCallback() on event handlers(like onClick) can help avoid recreating these handlers on every render.

  • useCallback() Helps in preventing the creation of new function instances on every render, which can improve performance in components that re-render frequently.

Simple Example: Imagine you have a counter component, and you pass an increment function to a button component:

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

function IncrementButton({ onIncrement }) {
  console.log('Button rendered');
  return <button onClick={onIncrement}>Increment</button>;
}

export default function Counter() {
  const [count, setCount] = useState(0);

  const increment = useCallback(() => {
    setCount(count + 1);
  }, [count]);

  return (
    <div>
      <p>Count: {count}</p>
      <IncrementButton onIncrement={increment} />
    </div>
  );
}

The increment function is created and passed to the IncrementButton component. If count doesn’t change, the increment function remains the same, so IncrementButton doesn’t re-render unnecessarily. If count changes, useCallback recreates the increment function, and IncrementButton re-renders with the new function.

useReducer

useReducer is a tool in React that helps you manage complex state logic in your components. Think of it as a way to handle state that involves multiple steps or rules. It’s like a more powerful version of useState that can handle more complicated situations. Syntax:

const [state, dispatch] = useReducer(reducer, initialState);

The following example can explain the best:

import React, { useReducer } from 'react';

// Define the reducer function
function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    default:
      throw new Error('Unknown action type');
  }
}

export default function Counter() {
  // Use useReducer with the reducer function and initial state
  const [state, dispatch] = useReducer(reducer, { count: 0 });

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>Decrement</button>
    </div>
  );
}

Reducer Function: This function says what to do when actions like 'increment' or 'decrement' happen. For example, if the action is 'increment', it adds 1 to the count.

The initial state is { count: 0 }. useReducer returns the current state and a dispatch function to send actions. The buttons use the dispatch function to send actions (increment and decrement), which the reducer function handles to update the state.

When to use:

  • Complex State Logic: When you have more complex state transitions or when the next state depends on the previous state.

  • Multiple State Values: When your state consists of multiple sub-values or is structured as an object.

  • State Management: When you need a more predictable way to manage state transitions.

useRef

The useRef hook provides a way to persist values across renders. But it doesn't cause a re-render when the value changes. It returns a mutable ref object whose .current property is initialized to the passed argument (initialValue). The ref object persists for the lifetime of the component.

Primary Uses ofuseRef:

Accessing DOM Elements:useRef is often used to directly access and interact with DOM elements. This is useful when you need to manage focus, select text, or perform other direct DOM manipulations.

Persisting Values: It can hold any mutable value that you want to persist across renders and doesn't cause re-renders. For example, it can be used to keep track of intervals, timeouts, or other mutable values that change over time.

Basic Example: Accessing DOM Elements:

import React, { useRef, useEffect } from 'react';

function FocusInput() {
  const inputRef = useRef(null);

  useEffect(() => {
    // Focus the input element when the component mounts
    inputRef.current.focus();
  }, []);

  return <input ref={inputRef} type="text" />;
}

export default FocusInput;

useRef(null) creates a ref object with the initial value null. The ref attribute on the <input> element is assigned to inputRef. The useEffect hook focuses the input element when the component mounts by calling inputRef.current.focus().

useImperativeHandle

The useImperativeHandle hook is used to control and manage imperative actions on child components from parent components. This is useful when you need to interact with the child component in an imperative manner (e.g., triggering a focus, playing a video, or manually managing animations).

Normally, ref provides direct access to a DOM node or a class component instance. useImperativeHandle allows you to define what is exposed when the parent component uses a ref to interact with the child component.

How It Works:useImperativeHandle is used within a functional component. It takes three arguments:

  • ref: The ref object passed from the parent component.

  • createHandle: A function that returns the object you want to expose to the parent component.

  • [deps]: An optional array of dependencies. The handle is re-created if any dependency changes.

Example Scenario: Consider a custom input component where you want to expose a focus method to the parent component. This allows the parent to programmatically focus the input field.

// Child Component
import React, { useImperativeHandle, useRef, forwardRef } from 'react';

const CustomInput = forwardRef((props, ref) => {
  const inputRef = useRef(null);

  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    },
    clear: () => {
      inputRef.current.value = '';
    }
  }));

  return <input ref={inputRef} {...props} />;
});

export default CustomInput;
// Parent Component
import React, { useRef } from 'react';
import CustomInput from './CustomInput';

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

  const handleFocus = () => {
    inputRef.current.focus();
  };

  const handleClear = () => {
    inputRef.current.clear();
  };

  return (
    <div>
      <CustomInput ref={inputRef} />
      <button onClick={handleFocus}>Focus Input</button>
      <button onClick={handleClear}>Clear Input</button>
    </div>
  );
}

export default ParentComponent;

CustomInput is wrapped with forwardRef to forward the ref from the parent to the child component. Inside CustomInput, useImperativeHandle is used to define a custom handle. This handle exposes two methods: focus and clear. Rest of the story is done by useRef() hook.

useDeferredValue

useDeferredValue() hook allows you to defer updates to a value. The value passed to useDeferredValue will be deferred, meaning React will prioritize more urgent updates over it. The deferred value is updated at a lower priority, helping to avoid blocking the main thread during critical interactions. By deferring less critical updates, useDeferredValue helps keep the UI responsive, ensuring smoother user experiences during high-priority tasks like typing or clicking.

Example Scenario: Suppose you're building a search component that filters a large list of items based on user input. Each keystroke updates the search query and triggers a re-render of the list. Without deferring, each keystroke might cause the list to re-render immediately. As the component has a large list of items to be re-rendered, it could take a while and in the mean time the next render could start which could lead to lags.

By using useDeferredValue you can defer the update of the filtered list until the continuous rendering gets to complete. Thus the input remains responsive.

import React, { useState, useDeferredValue, useMemo } from 'react';

function FilteredList({ items }) {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);

  const filteredItems = useMemo(() => {
    // Simulate a heavy computation
    return items.filter(item => item.toLowerCase().includes(deferredQuery.toLowerCase()));
  }, [items, deferredQuery]);

  return (
    <div>
      <input
        type="text"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search..."
      />
      <ul>
        {filteredItems.map((item, index) => (
          <li key={index}>{item}</li>
        ))}
      </ul>
    </div>
  );
}

export default FilteredList;

useState is used to manage the query state, which represents the user's input. useDeferredValue is used to create a deferred version of query. This means deferredQuery will lag behind query when React prioritizes other updates. useMemo is used to memoize the filteredItems computation, ensuring it only re-computes when items or deferredQuery changes. This simulates a heavy computation that should not block the main thread. The input field updates instantly as the user types, keeping the UI responsive. The filtering operation on items uses the deferred query, allowing the UI to remain responsive during the filtering process.

useTransition

The useTransition() hook allows you to mark certain state updates as "transitions," allowing the UI to remain responsive by prioritizing urgent updates (like user input) over less critical ones (like data fetching or rendering large lists). Basically this hook allows smoother user interactions by avoiding blocking the main thread with heavy computations or renders.

How It Works: useTransition returns an array with two elements: a startTransition function and isPending boolean. You can wrap your state updates inside the startTransition function to mark them as transitions (less priority code). Which means the part wrapped with startTransition starts to work after the continuous process completes.

Example: Here's a practical example illustrating the use of useTransition:

import React, { useState, useTransition, useMemo } from 'react';

function FilteredList({ items }) {
  const [query, setQuery] = useState('');
  const [isPending, startTransition] = useTransition();

  const filteredItems = useMemo(() => {
    // Simulate a heavy computation
    return items.filter(item => item.toLowerCase().includes(query.toLowerCase()));
  }, [items, query]);

  const handleChange = (e) => {
    const value = e.target.value;
    startTransition(() => {
      setQuery(value);
    });
  };

  return (
    <div>
      <input
        type="text"
        value={query}
        onChange={handleChange}
        placeholder="Search..."
      />
      {isPending && <p>Loading...</p>}
      <ul>
        {filteredItems.map((item, index) => (
          <li key={index}>{item}</li>
        ))}
      </ul>
    </div>
  );
}

export default FilteredList;

useState is used to manage the query state, representing the user's input. useTransition returns startTransition and isPending. startTransition marks the state update as a transition, and isPending indicates if the transition is in progress. useMemo is used to memoize the filteredItems computation, ensuring it only re-computes when items or query changes. This helps simulate a heavy computation that should not block the main thread. The handleChange function wraps the state update within startTransition, ensuring the update is performed as a transition. This keeps the input field responsive. isPending is used to show a loading indicator while the transition is ongoing.

Last Words

These hooks are some of the most important yet often less understood aspects of React. I hope you grasp their concepts as I do. I often found myself confused about how to use these hooks effectively. To clarify my understanding, I carefully read several articles and watched some excellent tutorials, notably from WebDevSimplified and Cosden Solutions. I then compiled everything into this comprehensive note, which serves as a reference for both you and me.

0
Subscribe to my newsletter

Read articles from Abeer Abdul Ahad directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Abeer Abdul Ahad
Abeer Abdul Ahad

I am a Full stack developer. Currently focusing on Next.js and Backend.