Understanding React Hooks: A Comparison of useEffect, useCallback, and useMemo

The Frontend Forge
7 min readJun 24, 2023

--

React Hooks were introduced in React 16.8 as a way to use state and other React features in functional components. Prior to Hooks, we had to use class components to access lifecycle methods and manage states. We can achieve the same functionalities in functional components with Hooks, making the code more concise and easier to understand.

In the functional react world, hooks have revolutionized the way we write reusable and efficient code blocks.

Photo Credit: Lautaro Andreani

What are Hooks?

Hooks are functions that enable us to use state and other React features in functional components. They allow us to reuse stateful logic without changing the component hierarchy.

They are functions that enable us to “hook into” React features and manage state and side effects in functional components. Hooks provide a simpler and more concise way of writing reusable and testable code.

React provides several built-in Hooks, such as useState, useEffect, useCallback, and useMemo, among others.

Some of the advantages hooks offer over traditional class components include:

  • Simpler code: Hooks eliminate the need for class components and provide a more straightforward approach to managing state and side effects.
  • Code reuse: Hooks allow you to reuse stateful logic across multiple components, reducing code duplication.
  • Improved readability: Hooks make it easier to understand and reason about the behaviour of individual components.
  • Testability: Hooks enable easier testing of individual functions and components in isolation.
  • Performance optimizations: Hooks like useCallback and useMemo offer performance benefits by memoizing values and preventing unnecessary re-rendering.

The useEffect Hook

The useEffect Hook is used to perform side effects in functional components. Side effects may include data fetching, subscriptions, or manually changing the DOM. The Hook takes two arguments: a function and a dependency array.

When the component renders, the function passed to useEffect is executed. It runs after every render by default. However, by specifying dependencies in the array, you can control when the effect is executed. If the dependencies remain the same between renders, the effect will not run again.

import React, { useState, useEffect } from 'react';
import axios from 'axios';

const DataComponent = ({ id }) => {
const [data, setData] = useState(null);

useEffect(() => {
// Fetch data from the API when the component first renders
const fetchData = async () => {
try {
const response = await axios.get(`/api/data/${id}`);
setData(response.data);
} catch (error) {
// Handle error if the API request fails
console.error('Error fetching data:', error);
}
};

fetchData();
}, [id]);

useEffect(() => {
// Update the component when the 'id' prop changes
const handlePropChange = () => {
// Perform specific logical actions based on the 'id' prop
if (id === 'some-value') {
// Do something
} else {
// Do something else
}
};

handlePropChange();

// Cleanup function
return () => {
// Perform cleanup actions when the component unmounts or 'id' prop changes
if (id === 'cleanup-value') {
// Perform specific cleanup logic
// ...
} else {
// Perform different cleanup logic
// ...
}
};
}, [id]);

return (
<div>
{/* Render the fetched data */}
{data ? (
<div>{data}</div>
) : (
<div>Loading data...</div>
)}
</div>
);
};

In the above example code block, the DataComponent fetches data from an API when it first renders by using the useEffect hook. The effect function is defined within the useEffect hook and makes an asynchronous call to retrieve the data. Once the data is fetched successfully, it is stored in the component's state using the setData function.

The useEffect hook also takes a dependency array [id], which indicates that the effect should be re-run whenever the id prop changes. This ensures that the component is updated with new data whenever the id prop is modified.

Additionally, the example demonstrates the usage of a cleanup function within the useEffect hook. The cleanup function is executed when the component unmounts or when the id prop changes. It allows you to perform any necessary cleanup, such as cancelling pending requests or unsubscribing from event listeners.

By utilizing the useEffect hook, you can handle side effects, such as data fetching, in a declarative and controlled manner, ensuring that the component stays up-to-date with the latest data and performs necessary cleanup when needed.

Common Use Cases

  • Data fetching: Use useEffect to fetch data from an API when the component mounts or when specific dependencies change.
  • Subscriptions: Manage subscriptions to external data sources or event emitters using useEffect.
  • DOM manipulation: If you need to interact directly with the DOM, useEffect is the right hook to use.

The useCallback Hook

The useCallback Hook is used to memoize functions and prevent unnecessary re-renders in child components. It returns a memoized version of the callback function that only changes if one of the dependencies has changed. This is useful when passing callbacks to child components to prevent them from re-rendering unnecessarily.

When a component renders, functions defined inside it are re-created, leading to potential performance issues in child components that rely on these functions.

By using useCallback, you can memoize a function and ensure that it is only re-created when its dependencies change. This optimization can significantly improve the performance of your application, especially in scenarios where callbacks are passed down multiple levels of the component tree.

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

const ParentComponent = () => {
const handleClick = useCallback((itemId) => {
// Perform specific action based on the itemId
if (itemId % 2 === 0) {
console.log(`Item with ID ${itemId} is even.`);
} else {
console.log(`Item with ID ${itemId} is odd.`);
}
}, []);

return (
<div>
{items.map((item) => (
<ChildComponent
key={item.id}
item={item}
onClick={handleClick}
/>
))}
</div>
);
};

In this above example, the handleClick function inside the useCallback hook performs a logical action based on the itemId. In this case, it checks if the itemId is even or odd and logs a corresponding message to the console.

By memoizing the handleClick function using useCallback, you ensure that it is only recreated when its dependencies change (in this case, there are no dependencies indicated by the empty dependency array []). This optimization prevents unnecessary re-renders of the child components and improves performance.

The memoized handleClick function can then be passed as a prop to the ChildComponent instances, allowing them to trigger the logical action when the associated button is clicked.

Common Use Cases

  • Optimizing child components: Use useCallback to prevent unnecessary re-rendering of child components that rely on function references for props.
  • Event handlers: When passing event handlers as props or as an action such as onClick actions, useCallback ensures that the reference remains stable, preventing unnecessary re-renders.

The useMemo Hook

The useMemo hook is similar to useCallback, but instead of memoizing functions, it memoizes values.

It is used to memoize values and prevent unnecessary re-computations. It takes a function and a dependency array as arguments. The function passed to useMemo will only recompute the memoized value when one of the dependencies has changed. This is useful for optimizing expensive calculations or complex operations.

import React, { useMemo } from 'react';

const ItemList = ({ data, options }) => {
const expensiveCalculationResult = useMemo(() => {
// Perform complex and computationally expensive calculation
let result = [];

for (let i = 0; i < data.length; i++) {
// Perform some intensive operations on each item
// ...
// Example: Concatenate the item name with the options value
const modifiedItem = {
...data[i],
nameWithOptions: `${data[i].name} - ${options}`,
};

result.push(modifiedItem);
}

return result; // The calculated result
}, [data, options]);

return (
<div>
{expensiveCalculationResult.map((item) => (
<div key={item.id}>{item.nameWithOptions}</div>
))}
</div>
);
};

In the above example, the expensiveCalculationResult is computed inside the useMemo function. The calculation involves iterating over each item in the data array and performing some intensive operations on each item. In this case, the example shows how to concatenate the item's name with the value of the options variable.

By memoizing the result using useMemo, the expensive calculation will only be performed again if either the data or options dependencies change. Otherwise, the previously memoized result will be returned, avoiding unnecessary computations and improving the performance of the component.

Common Use Cases

  • Expensive computations: Use useMemo to avoid redundant calculations that are computationally expensive.
  • Value caching: If a value doesn’t change frequently and its calculation is resource-intensive, useMemo can cache the result until dependencies change.

Differences between useEffect, useCallback, and useMemo

While useEffect, useCallback, and useMemo are all Hooks used in React, they serve different purposes:

  • useEffect is used for handling side effects and is executed after every render.
  • useCallback is used mainly for memoizing functions and preventing unnecessary re-renders in child components.
  • useMemo is used for memoizing values and optimizing expensive calculations or complex operations.

These Hooks have different use cases and should be chosen based on the specific requirements of your application.

Key Takeaways

To make the most of these Hooks, consider the following best practices:

  1. Use useEffect for data fetching, subscriptions, and other side effects.
  2. Use useCallback when passing callbacks to child components to prevent unnecessary re-renders.
  3. Use useMemo for optimizing expensive calculations or complex operations.
  4. Carefully choose the dependencies for each Hook to ensure they are accurate and up-to-date.
  5. Avoid unnecessary re-renders by using the appropriate Hook for each scenario.

Understanding the difference between useEffect, useCallback, and useMemo hooks is crucial for us as React developers. These hooks provide powerful tools for managing state, side effects, and performance optimizations. By leveraging the right hook in the appropriate scenario, you can write cleaner, more efficient code and enhance the overall user experience of your React applications.

--

--

The Frontend Forge
The Frontend Forge

Written by The Frontend Forge

Sharpen your frontend skills at The Frontend Forge! 🔥 Join me as I break down JS, TS, React, Vue, React Native, & Next.js with insightful articles & tuts. 💻

No responses yet