Exploring React Hooks: A Comprehensive Tutorial Series — Part 4: useCallback
Elevate Your React Development with Hooks — Simplifying State and Lifecycle Management
Hi there 👋,
Welcome to the fourth part of the React Hooks series! In this part we will explore about useCallback
hook with the practical explanation. Let’s deep dive into the topic 🎊
useCallback
The useCallback
hook in React is crucial for optimizing the performance of components that rely heavily on functions, especially when these functions are passed as props to child components. Similar to useMemo
, which memoizes the result of a function, useCallback
returns a memorized version of the callback function itself. This prevents unnecessary re-renders of child components that depend on these functions if the function dependencies have not changed.
Development Needs for useCallback
In complex React applications, especially those with deep component trees, passing callbacks deep down the component tree can lead to performance issues if these components re-render too often. This is typically because functions are re-created on every render, making shallow comparison of props fail in child components, leading to unnecessary renders. useCallback
helps mitigate this by ensuring that function identities are stable across renders if their dependencies remain unchanged.
Example 1:
let’s consider a simple button click scenario where a callback function is needed to update a count:
import { useState, useCallback } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const increment = useCallback(() => {
setCount(c => c + 1);
}, []); // Dependencies array is empty, meaning the function never changes
return (
<button onClick={increment}>Increment</button>
);
}
export default Counter;
In this example, the increment
function is wrapped in useCallback
with an empty dependency array, indicating that it does not depend on any values from the component scope and should remain the same for the lifetime of the component. This is particularly useful if this function were to be passed to deeply nested child components.
Example 2:
Now, let’s a simple application with a list of users, a shuffle button, and a search input to filter users by name. The key focus is on how useCallback
can be used to prevent unnecessary re-renders of components that receive functions as props:
import { useCallback, useState } from 'react';
import Search from './_components/Search';
const allUsers = ['Vinojan', 'Abhimanyu', 'Aji', 'Vino', 'Jaksha'];
const shuffle = (array: string[]) => {
let currentIndex = array.length, randomIndex;
// Create a copy of the array to shuffle to avoid mutating the original array
let arrayCopy = [...array];
while (currentIndex !== 0) {
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex--;
[arrayCopy[currentIndex], arrayCopy[randomIndex]] = [
arrayCopy[randomIndex], arrayCopy[currentIndex]];
}
return arrayCopy;
};
const Users = () => {
const [users, setUsers] = useState(allUsers);
const handleSearch = (text: string) => {
const filteredUsers = allUsers.filter((user) =>
user.toLowerCase().includes(text.toLowerCase()),
);
setUsers(filteredUsers);
}
return (
<div className='tutorial'>
<div className='align-center mb-2 flex'>
<button onClick={() => setUsers(shuffle(allUsers))}>
Shuffle
</button>
<Search onChange={handleSearch} />
</div>
<ul>
{users.map((user) => (
<li key={user}>{user}</li>
))}
</ul>
</div>
);
}
export default Users
// /_components/search.tsx
import { memo } from 'react';
interface SearchProps {
onChange: (text: string) => void;
}
const Search = ({ onChange }: SearchProps) => {
console.log('Search rendered'); // To monitor when the Search component re-renders
return (
<input
type="text"
placeholder="Search users..."
onChange={(e) => onChange(e.target.value)}
/>
)
}
export default memo(Search);
`memo`
The
memo
function is a higher-order component provided by React for optimizing performance, especially in components that receive props. It is primarily used to prevent unnecessary re-renders of functional components by memorizing the result of a render based on specific inputs (usually props).When a component is wrapped with
memo
, React performs a shallow comparison of the previous and new props. If the comparison results show that the props have not changed, React skips rendering the component and reuses the last rendered result.
Here, If theonChange
function is not changed, theSearch
component will not be re-rendered.Use Cases: It is most beneficial in components that:
- Receive props that frequently have the same values.
- Are relatively expensive to render.
- Render frequently due to parent component re-renders.
So here, We have to be wrapped the handleSearch
with useCallback
to ensure it retains the same reference across renders unless its dependencies change. This is crucial because the Search
component is wrapped with memo
, which only re-renders if its props change.
// other code
const handleSearch = useCallback((text: string) => {
console.log('First user', users[0]); // Output: always 'Vinojan'
const filteredUsers = allUsers.filter((user) =>
user.toLowerCase().includes(text.toLowerCase()),
);
setUsers(filteredUsers);
}, [users]);
// This function will be freezed on render this component, After that its memorizing the function.
// To freeze the function, remove the users into the dependancy array, it means handleSearch won't change
// return function
NOTE: It’s essential to correctly manage the dependencies of useCallback
. If handleSearch
used any state or props that change over time, those should be included in the dependency array to ensure that the function updates when needed.
How the Frontend Development Became Easier After the useCallback
useCallback
has made frontend development more efficient by:
- 🔄 Reducing Render Cycles: Ensuring function identities are stable reduces unnecessary renders in components that receive these functions as props.
- 🛠️ Optimizing Performance in Deep Component Trees: In applications with many nested components,
useCallback
prevents a ripple effect of re-renders through the component tree.
As we have seen, we covered the practical use and benefits of useCallback
in optimizing React applications. This understanding is crucial for writing efficient React code, especially in larger applications with complex component trees.
I appreciate you taking the time to read this article.🙌
🔔 Stay tuned for the next article in this series, where we will explore useContext
, another powerful hook for managing global state in a React application. This will help simplify state management across different levels of the component tree, reducing prop drilling.
Before you move on to explore next article, don’t forget to give your claps 👏 for this article and leave your feedback💭 on the comment section.
Stay connected with me on social media. Thanks for your support and have a great rest of your day! 🎊
Part 1: useState — https://medium.com/@imvinojanv/exploring-react-hooks-a-comprehensive-tutorial-series-part-1-usestate-3c1ce8378e95
Part 2: useEffect — https://medium.com/@imvinojanv/exploring-react-hooks-a-comprehensive-tutorial-series-part-2-useeffect-dd2d63778af8
Part 3: useMemo — https://medium.com/@imvinojanv/exploring-react-hooks-a-comprehensive-tutorial-series-part-3-usememo-6c694f485d95
✍️ Vinojan Veerapathirathasan.
LinkedIn : https://www.linkedin.com/in/imvinojanv/
Email: imvinojanv@gmail.com