Optimizing React Components: A Deep Dive into useRef, useReducer, useMemo, and useCallback

Hey, this is my second tutorial about React hooks. Have a look at my first blog post which describes about useState, useEffect, and useContext React hooks, if you missed it. As I mentioned in my previous tutorial, Hooks are nothing but functions that let you “hook into” React state and lifecycle features from function components. Let's explore more about React hooks.

1. useReducer Hook

The useReducer hook is a React hook that provides an alternative to useState for managing complex state logic in functional components. It's particularly useful when you have complex stateful logic that involves multiple sub-values or when the next state depends on the previous one.

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

state: The current state of the reducer.

dispatch: A function that you can call to dispatch an action, which triggers a state update.

reducer: A function that takes the current state and an action, then returns the new state.

initialState: The initial state of the reducer.

Example

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();
  }
}


export default function App() {
  const [state, dispatch] = useReducer(reducer, { count: 0 });
  return (
    <div className='Counter'>
      Count: {state.count}
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
    </div>
  );
}

2. useRef Hook

The useRef Hook allows you to store values between renderings. It can be used to hold a mutable value that does not require a re-render when modified. It provides direct access to a DOM element. Let’s have a look at a few use cases.

Count how many times our application renders

Using the useState hook to count renders of a component will indeed lead to an infinite loop because updating state within the render phase of a component causes the component to re-render. Each re-render increments the count, triggering another re-render, and so on. To count renders without causing an infinite loop, you can use the useRef hook instead, which allows you to mutate a value without triggering a re-render:

function App() {
  const [count, setCount] = useState(0);
  const countUp = () => {
    setCount(count + 1);
  }
const renderCount = useRef(0);
  useEffect(() => {
    renderCount.current = renderCount.current + 1;
  });

  return (
    <div>
        <Button variant="contained" color="primary" onClick={countUp}>
        Count Up
      </Button>
      <p>{count}</p>
     <p>Render Count: {renderCount.current}</p>
    </div>
  );
}

Accessing DOM Elements

One common use of useRef is to directly interact with a DOM element in a functional component. This could be for focusing an input, measuring the size of an element, or any other direct DOM manipulation.

function TextInputWithFocusButton() {
  const inputEl = useRef(null);

  const onButtonClick = () => {
    // `current` points to the mounted text input element
    inputEl.current.focus();
  };

  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}

Storing a Mutable Value

Another use of useRef is to hold a value persistently across re-renders without causing a component update when its value changes. This is useful for tracking values over time without triggering re-renders, like storing the previous state, timeouts, or intervals.

function App() {
  const [name, setName] = useState("");
  const previousName = useRef("");

  useEffect(() => {
    previousName.current = name;
  }, [name]);

  return (
    <>
      <input
        type="text"
        value={name}
        onChange={(e) => setName(e.target.value)}
      />
      <h2>Current Name: {name}</h2>
      <h2>Previous Name: {previousName.current}</h2>
    </>
  );
}

3. useMemo Hook

The useMemo hook is a performance optimization tool in React that memorizes (caches) the result of a function. This means it remembers the result of a function given a set of inputs and only recalculates the result when one of the inputs has changed. useMemo is useful for avoiding expensive calculations on every render when the inputs have not changed.

useMemo accepts two arguments:

1. A "create" function that performs the expensive calculation and returns its result.

2.An array of dependencies, similar to useEffect, which useMemo uses to determine whether the memoized value should be recalculated.

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

Example

import React, { useState, useMemo } from 'react';
import Button from '@mui/material/Button';


const expensiveCalculation = (num) => {
  console.log("Calculating...");
  for (let i = 0; i < 1000000000; i++) {
    num += 1;
  }
  return num;
};

function App() {

  const [cowCount, setCowCount] = useState(0);
  const [lambsCount, setLambsCount] = useState(0);
  const result = useMemo(() =>expensiveCalculation(cowCount),[cowCount]);

  const countUpCows = () => {
    setCowCount(cowCount + 1);
  }

  const countUpLambs = () => {
    setLambsCount(lambsCount + 1);
  }

  return (
    <div>
      <Button variant="contained" color="primary" onClick={countUpLambs}>
        Count Up Lambs: {lambsCount}
      </Button>
      <Button variant="contained" color="primary" onClick={countUpCows}>
        Count Up Cows: {cowCount}
      </Button>
      <h4>Calculation Result: {result}</h4>
    </div>
  );
}

export default App;

Here, the expensiveCalculation function only depends on cowsCount. We don’t need to execute it when lambsCount is changed. It is achieved with the useMemo hook. Just as useEffect, if the dependency list is empty, it is executed only in the initial render. If dependency is not given, it is executed in each render. So, using the useMemo without a dependency list makes no sense at all!

4. useCallback Hook

The useCallback hook in React is used to memoize functions, helping to prevent unnecessary re-renders and optimizations, especially in components with heavy rendering or when passing callback functions to optimized child components.

Example

import React, { useState, useCallback } from 'react';
import ChildComponent from './ChildComponent';

function App() {

  const [count, setCount] = useState(0);
  const [name, setName] = useState('Flavio');

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

  return (
    <div>
      <h1>Count: {count}</h1>
      <ChildComponent onIncrement={incrementCount} />
      <input value={name} onChange={(e) => setName(e.target.value)} />
    </div>
  );
}

export default App;
import React from 'react';

const ChildComponent = React.memo(({ onIncrement }) => {
  console.log('ChildComponent rendered');
  return (
    <button onClick={onIncrement}>Increment</button>
  );
});

export default ChildComponent;

Here, ChildComponent is rendered only if the count variable is changed. If we change the name, it’s not needed to render ChildComponent again. That is achieved with the useCallback function.

You may wonder, what is this React.memo in ChildComponent. React.memo is a higher-order component (HOC) provided by React for memorizing functional components. It wraps around a functional component, allowing React to skip rendering the component if its props have not changed between renders. This optimization can improve performance, especially in cases where components re-render often with the same props.

Drawbacks of useMemo and useCallback:

There's a small but notable overhead associated with using these hooks, as React needs to store the memoized values or functions in memory and check the dependency array on each render to determine whether to recalculate or recreate them. This overhead might outweigh the performance benefits in cases where the calculations are not particularly expensive or the functions are not passed to components that re-render often.

Thanks for reading! Happy Coding with React Hooks!

Read my next blog about: useRef, useReducer, useMemo, useCallback hooks