React is a popular JavaScript framework for front-end development that has made developers’ lives easier by introducing component-driven development.
Before React 16, components were usually defined with Class and had different lifecycle methods. React 16 promoted the creation of functional components and introduced the following hooks:
- useEffect for handling the component’s lifecycle.
- useState for state management.
These hooks are necessary because re-renders and re-computations are costly operations, so minimizing them improves the application’s performance.
React has recently introduced two hooks, useCallback and useMemo. Like the useEffect and useState hooks, these are potent tools that can help developers optimize the performance of their applications by reducing unnecessary re-renders and re-computations.
In this blog, we will delve into these hooks, exploring their functionality and examining how they work. We will also discuss the appropriate use cases for these hooks, providing practical examples to help you understand when and where to use them in your projects. Let’s get started!
useCallback hook
Syntax
const computedFn = useCallback(()=>{
doSomething(dependencies);
}, [dependencies]);
In the previous code example, doSomething(dependencies); will be returned and stored in the computedFn variable. This only occurs only when the dependencies change, and useCallback will then provide the new doSomething(dependencies); value.
This is useful when you want a function to avoid re-rendering whenever the component changes.
For example, we will use the debounce function on the input field to get the data only when the user stops writing for a specific amount of time.
import React, { useState } from "https://esm.sh/react@18.2.0";
import ReactDOM from "https://esm.sh/react-dom@18.2.0";
const App = () => {
const [text, setText] = useState("");
const debounce = (func, delay) => {
let inDebounce;
return function () {
const context = this;
const args = arguments;
clearTimeout(inDebounce);
inDebounce = setTimeout(() => func.apply(context, args), delay);
};
};
const makeApiCall = () => {
console.log(text, " Making an API call");
};
const debouncedApiCall = debounce(makeApiCall, 500);
const onChangeHandler = (e) => {
setText(e.target.value);
debouncedApiCall();
};
return (
<div>
<input type="text" onChange={onChangeHandler} value={text} />
</div>
);
};
ReactDOM.render(<App />, document.getElementById("root"));
Every time the state is updated, the component re-renders, and the debounce function is reattached to the debouncedApiCall variable. As a result, the logs are printed every time the state changes, and the debounce needs to be fixed.
To avoid this, we can wrap the debounce function inside useCallback , and the program will write the memoized debounce function that will change only if dependencies change.
import React, { useState, useCallback } from "https://esm.sh/react@18.2.0";
import ReactDOM from "https://esm.sh/react-dom@18.2.0";
const App = () => {
const [text, setText] = useState("");
const _debounce = (func, delay) => {
let inDebounce;
return function () {
const context = this;
const args = arguments;
clearTimeout(inDebounce);
inDebounce = setTimeout(() => func.apply(context, args), delay);
};
};
const makeApiCall = (e) => {
console.log(e, "Making an API call");
};
const debounce = useCallback(_debounce(makeApiCall, 2000), []);
const onChangeHandler = (e) => {
setText(e.target.value);
debounce(e.target.value);
};
return (
<div>
<input type="text" onChange={onChangeHandler} value={text} />
</div>
);
};
ReactDOM.render(<App />, document.getElementById("root"));
We don’t want to re-render the _debounce function, so we are not passing any dependencies. This code will print the log only when the user has stopped writing, and 2000 ms have passed. Notice in the code that we are passing the value to debounce(e.target.value) because it cannot pull the value of the state.
Even though it seems to be working fine, it is not. If you have ESLint enabled, you will notice that we are getting the following linting error for the useCallback hook:
error React Hook useCallback received a function whose dependencies are unknown. Pass an inline function instead react-hooks/exhaustive-deps
This is because the dependencies of the function that useCallback is taking as input are not known, so ESLint wants us to write an inline function.
Also, the _debounce function is re-created on every re-render, thus changing its reference and defeating the purpose of caching it.
To fix this, we can create a custom useDebounce hook and use it.
import { useState, useCallback, useRef } from "react";
const useDebounce = (func, delay) => {
const inDebounce = useRef();
const debounce = useCallback(
function () {
const context = this;
const args = arguments;
clearTimeout(inDebounce.current);
inDebounce.current = setTimeout(() => func.apply(context, args), delay);
},
[func, delay]
);
return debounce;
};
const App = () => {
const [text, setText] = useState("");
const makeApiCall = useCallback((e) => {
console.log(e, "Making an API call");
}, []);
const debounce = useDebounce(makeApiCall, 2000);
const onChangeHandler = (e) => {
setText(e.target.value);
debounce(e.target.value);
};
return (
<div>
<input type="text" onChange={onChangeHandler} value={text} />
</div>
);
};
useMemo hook
Syntax
const computedValue = useMemo(()=>{
doSomething();
}, [dependencies]);
useMemo works similarly to the useCallback , but rather than returning the function, it returns the computed value from the function, i.e. the function’s output, and only recomputes the function when the dependencies change to provide the new result.
For example, let’s say we have a child component that should render only when the text has a specific value.
import React, { useState, useCallback } from "https://esm.sh/react@18.2.0";
import ReactDOM from "https://esm.sh/react-dom@18.2.0";
const ChildComponent = ({ text }) => {
return <p>{text}</p>;
};
const App = () => {
const [text, setText] = useState("");
const onChangeHandler = (e) => {
setText(e.target.value);
};
return (
<div>
<input type="text" onChange={onChangeHandler} value={text} />
<ChildComponent text={text} />
</div>
);
};
ReactDOM.render(<App />, document.getElementById("root"));
If the conditional check were placed within the child component, it would trigger a re-render each time the parent’s state updates, regardless of our intention to limit it to certain values.
The useMemo hook addresses this. By employing this hook, the child component becomes memoized, ensuring it only re-renders when the conditions specified in its dependencies are satisfied.
In this instance, the child component will exclusively render when the text matches the term “syncfusion.”
import React, { useState, useMemo } from "https://esm.sh/react@18.2.0";
import ReactDOM from "https://esm.sh/react-dom@18.2.0";
const ChildComponent = ({ text }) => {
return <p>{text}</p>;
};
const App = () => {
const [text, setText] = useState("");
const onChangeHandler = (e) => {
setText(e.target.value);
};
const memoizedChildComponent = useMemo(() => <ChildComponent text={text} />, [text === 'syncfusion'])
return (
<div>
<input type="text" onChange={onChangeHandler} value={text} />
{memoizedChildComponent}
</div>
);
};
ReactDOM.render(<App />, document.getElementById("root"));
Conclusion
Thanks for reading. This blog delved into the two most potent React hooks, namely useCallback and useMemo , explaining their functions and exploring their optimal application to sidestep redundant computations when React components re-render.
I encourage you to explore these exceptional features and share your experience in the comment section below.
Have you experienced Syncfusion? Feel free to download the product setup here. If you’re new to Syncfusion, enjoy a complimentary 30-day trial.
Should you need assistance, don’t hesitate to reach out via our support forums, support portal, or feedback portal. We’re dedicated to aiding you at every step!