Dead Simple Chat allows you to easily add Chat to any React Application using powerful Javascript Chat SDK.
In this blog post, we will see how to use React useCallback
hook.
What is its purpose we will also look at real-world scenarios where it should be used and common pitfalls to avoid when using React useCallback
.
What is useCallback?
useCallback
is used to cache the function "definition". It is typically used in conjunction with React memo
.
If you have cached your component with React memo so that it doesn't re-render unless its props are changed.
If you are passing the component a function, then your function will re-render every time, defeating the purpose of using memo.
Because in Javascript function() {}
or () => { }
create a different function, thus the prop will never be the same, causing the component to re-render, make memo
useless.
To prevent this from happening, you can wrap the function definition inside useCallback
and this prevents the re-creation of the function unless the dependencies changes.
Syntax of useCallback
The useCallback
hook accepts two parameters, one is the method/function you would like to cache, and the second parameter is the dependency array, which returns the cached function.
When the variable passed in the dependency array changes, the useCallback
hook returns and updated function.
const method = useCallback(<METHOD>, [<DEPENDENCY_ARRAY]);
Let's look at some examples to understand it better.
Dead Simple Chat allows you to integrate chat in minutes into your React or web application using the powerful Javascript Chat SDK.
Example of useCallback
Consider the following code:
import React, { useState } from "react";
const ChildComponent = React.memo(({ onButtonClick }) => {
console.log("ChildComponent rendered");
return <button onClick={onButtonClick}>Increment</button>;
});
function ParentComponent() {
const [count, setCount] = useState(0);
const [theme, setTheme] = useState("light");
const handleButtonClick = () => {
setCount(count + 1);
};
return (
<div>
<h1>Current Theme: {theme}</h1>
<button
onClick={() => {
theme === "light" ? setTheme("dark") : setTheme("light");
}}
>
Toggle Theme
</button>
<h1>Counter: {count}</h1>
<ChildComponent onButtonClick={handleButtonClick} />
</div>
);
}
export default ParentComponent;
In the above code we have created a ChildComponent
and cached it using React. memo
.
A Brief Primer on React Memo:
By using React.memo
the component will not re-render unless the props are changed.
Typically when the parent component is re-rendered all the child components are also re-rendered.
But by using React memo on a child component, when the parent component is re-rendered the Child Component is not re-rendered unless the props of the Child Component change.
In the ChildComponent
we have also added a console.log
statement to print on the console "ChildComponent rendered".
We are accepting a method onButtonClick
as a prop of the ChildComponent
and calling the method on the onClick
event when the button is pressed.
Next, we have created a ParentComponent
and in the ParentComponent
we have created two state variables, one is count
and another one is theme
.
In the ParentComponent
we have created a method called as handleButtonClick
which increments our state variable count
and we are passing this method as a prop to the ChildComponent
.
We have also created a button to toggle the second state variable called as the theme
and when the button is pressed we are toggling the theme from light to dark.
We are also displaying the current theme and count.
As we have used React.memo our expected behaviour would be when we toggle the theme, we should not see, the ChildComponent rendered
message on the screen.
Let's try it out:
As you can see in the above video, each time the "Toggle Theme" button is pressed the "ChildComponent rendered" message is printed on the screen.
Why is this happening?
We had discussed in the React useCallback intro, it happens because JavaScript creates a new function each time it the component is rendered.
And we are passing the function as a prop, and React memo will see it as a new function and re-render the child component.
To solve this problem we will wrap our function in useCallback
and it will return a cached version of our function.
Here is the updated code:
import React, { useState, useCallback } from "react";
const ChildComponent = React.memo(({ onButtonClick }) => {
console.log("ChildComponent rendered");
return <button onClick={onButtonClick}>Click me</button>;
});
function ParentComponent() {
const [count, setCount] = useState(0);
const [theme, setTheme] = useState("light");
// Using useCallback to cache the function
const handleButtonClick = useCallback(() => {
setCount(count + 1);
}, [count]);
return (
<div>
<h1>Current Theme: {theme}</h1>
<button
onClick={() => {
theme === "light" ? setTheme("dark") : setTheme("light");
}}
>
Toggle Theme
</button>
<h1>Counter: {count}</h1>
<ChildComponent onButtonClick={handleButtonClick} />
</div>
);
}
export default ParentComponent;
In our updated code we have wrapped the function inside useCallback
hook. Let's see how our updated code performs:
As you can see in the console, the "ChildComponent rendered" message is not printed each time we press the "Toggle Theme" button.
Let's look at some real-world scenarios where to use useCallback
.
Real-world Scenarios of useCallback
We will look at some real-world scenarios with code examples where using useCallback
would be useful.
Memoize Data Fetching Function in Infinite Scroll
In an infinite scrolling list we can use the useCallback
to cache the function that is responsible to fetch the data to prevent unnecessary renders and API calls.
Let's look at an example, we will build a simple infinite scrolling listing using the freely available Github List Users API.
In this example, we will use useCallback
and useEffect
in conjunction to prevent unnecessary API calls.
import { useState, useEffect, useCallback } from "react";
function InfinitUserScroll() {
const [users, setUsers] = useState([]);
const [page, setPage] = useState(1);
const fetchData = useCallback(async () => {
const response = await fetch(
`https://api.github.com/users?since=${page * 30}`
);
const nextData = await response.json();
setUsers((curData) => [...curData, ...nextData]);
}, [page]);
useEffect(() => {
fetchData();
}, [fetchData]);
const handleScroll = (e) => {
const { scrollTop, scrollHeight, clientHeight } = e.target;
if (scrollHeight - scrollTop === clientHeight) {
setPage((prevPage) => prevPage + 1);
}
}
return (
<>
<div
onScroll={handleScroll}
style={{ overflowY: "scroll", height: "400px" }}
>
<h1>Github Users</h1>
<hr />
{users.map((item, index) => (
<div key={index}>{item.login}</div>
))}
</div>
</>
);
}
export default InfinitUserScroll;
In the above code we are using useCallback
to cache methods one is the fetchData
method.
The fetchData
method is cached and page
state variable is the dependency passed to the useCallback caching the fetchData method.
Then we are passing fetchData
as a dependency in useEffect
and calling fetchData
method in useEffect
.
Listing page
as a dependency in useCallback
causes the fetchData
method to be re-created only when the page
value changes.
When the page
value changes, new fetchData
method is created which causes the useEffect
to trigger to fetch the new page info.
Util the page value is updated the fetchData
method is not called thus preventing unnecessary API calls.
Debouncing Input to prevent excessive calls to API
We can also use useCallback
hook to debounce the user input that calls an API for example a search API.
Debouncing the user input prevent excessive calls to the API, thus preventing the server from being overloaded or if you are using a 3rd party API prevents the API costs.
Let's code at the code example:
import React, { useState, useEffect, useCallback } from "react";
import debounce from "lodash.debounce";
function WikiSearch({ searchDelay = 300 }) {
const [searchTerm, setSearchTerm] = useState("");
const [results, setResults] = useState([]);
const performSearch = async (term) => {
const response = await fetch(
`https://en.wikipedia.org/w/api.php?action=query&list=search&format=json&origin=*&srsearch=${term}`
);
const data = await response.json();
setResults(data.query.search);
};
const debouncedSearch = useCallback(
debounce((term) => {
performSearch(term);
}, searchDelay),
[searchDelay]
);
useEffect(() => {
if (searchTerm) {
debouncedSearch(searchTerm);
} else {
setResults([]);
}
}, [searchTerm, debouncedSearch]);
return (
<div>
<h3>Wiki Search Engine</h3>
<hr />
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search Wikipedia."
/>
{results.length > 0 ? (
<h1>Wikipedia Search Results</h1>
) : (
<div>
<br />
<strong>Nothing Found</strong>
</div>
)}
<ul>
{results.map((result) => (
<li key={result.pageid}>
<a
href={`https://en.wikipedia.org/?curid=${result.pageid}`}
target="_blank"
rel="noreferrer"
>
{result.title}
</a>
</li>
))}
</ul>
</div>
);
}
export default WikiSearch;
In the above code example, we are using the Wikipedia API to search Wikipedia to fetch a list of pages.
We are adding a default debounce of 300ms and using the lodash.debounce
library and creating a cached de-bounced method using useCallback
We have created a WikiSearch
component that accepts an optional searchDelay
.
Next, we have created a method called as performSearch
, the method will fetch the information and set it in the results date variable.
Using the useCallback
, we have created a debouncedSearch method and called the useCallback
hook to debounce
the search.
Then finally in the useEffect
hook we are calling the debouncedSearch
method.
Here is the demo:
Handling Events in a List
When you a list of items with each item has an event handler, then we can use the useCallback
to cache the handler function.
To demonstrate this we will create a TodoList component, that will display a list of Todos.
import React, { useState, useCallback } from "react";
const TodoItem = React.memo(({ item, onToggle }) => (
<li>
<input
type="checkbox"
checked={item.completed}
onChange={() => onToggle(item.id)}
/>
{item.name}
</li>
));
function TodoListComponent() {
const [todos, setTodos] = useState([
{ id: 1, name: "Todo 1", completed: false },
{ id: 2, name: "Todo 2", completed: false },
{ id: 3, name: "Todo 3", completed: false },
{ id: 4, name: "Todo 4", completed: false }
]);
const handleToggle = useCallback((id) => {
setTodos((prevTodos) =>
prevTodos.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
}, []);
return (
<ul>
{todos.map((todo) => (
<TodoItem key={todo.id} item={todo} onToggle={handleToggle} />
))}
</ul>
);
}
export default TodoListComponent;
We have created the handleToggle
method and caching it using useCallback
, and passing the handleToggle
to the <TodoItem />
list.
Conclusion
In this blog post, we have learned how to use useCallback
and the real-world scenarios and examples.