Table of Contents
Let’s list some reasons why we even need a useCallback to begin with.
- Each re-render wouldn’t unnecessarily re-create the function.
- In case we actually need to re-evaluate the function, we pass it as a reactive dependency.
Avoid Unneccesary Re-renders
Lets take below code for example, the increment button increases the counter value by 1 and we avoided the re-renders on state change with our useCallback hook.
import { useCallback, useState } from "react"
const buttonStyle = { padding: "8px 16px", margin: "8px", borderRadius: "6px", backgroundColor: "#4CAF50",} as const;
export default function Sample() { const [counter, setCounter] = useState(0) const increment = useCallback((e) => { setCounter(x => x+1) }, []) return <div style={{height: "400px", width: "600px", margin: "auto", marginTop: "100px"}}> Current Value: {counter} <div> <button onMouseDown={(e) => { e.currentTarget.style.transform = "scale(0.95)"; e.currentTarget.style.boxShadow = "0 2px 4px rgba(0,0,0,0.2)"; }} onMouseUp={(e) => { e.currentTarget.style.transform = "scale(1)"; e.currentTarget.style.boxShadow = "0 2px 4px rgba(0,0,0,0.2)"; }} style={buttonStyle} onClick={increment}> Increment </button> </div> </div>}
useCallback
Re-evaluating the function on state change based on some state update
Lets update our code to support a use-case where our the state of our component gets changed after 5 seconds.
After 5 seconds have elapsed, we should increment our numbers by 2.
In real applications, this state could be an API response or some other event triggered from the other component in the DOM.
import { useCallback, useEffect, useState } from "react"
const buttonStyle = { padding: "8px 16px", margin: "8px", borderRadius: "6px", backgroundColor: "#4CAF50",} as const;
export default function Sample() { const [counter, setCounter] = useState(0) const [incrementValue, setIncrementValue] = useState(1) const increment = useCallback((e) => { setCounter(x => x+incrementValue) }, [incrementValue]) useEffect(() => { const id = setTimeout(() => { setIncrementValue(2) console.log("Increment set to 2") }, 5000) return () => clearTimeout(id) }, []) return <div style={{height: "400px", width: "600px", margin: "auto", marginTop: "100px"}}> Current Value: {counter} <div> <button onMouseDown={(e) => { e.currentTarget.style.transform = "scale(0.95)"; e.currentTarget.style.boxShadow = "0 2px 4px rgba(0,0,0,0.2)"; }} onMouseUp={(e) => { e.currentTarget.style.transform = "scale(1)"; e.currentTarget.style.boxShadow = "0 2px 4px rgba(0,0,0,0.2)"; }} style={buttonStyle} onClick={increment}> Increment {incrementValue > 1 && `+${incrementValue}`} </button> </div> </div>}
Let’s understand what is being done here.
- We first introduced a new state variable,
incrementValue - Every time
incrementValuegets changed, we want ouruseCallbackto re-evaluate the function withincrementValue - We add a 5 seconds timer to change the
incrementValueto 2 and after 5 seconds, the increments will happen with 2.
Recreating “useCallback” with “useMemo”
Now that we now we simply keep a memoized version of a function, We can actually re-create useCallback with useMemo, with small changes you get below code with the exact same behavior.
import { useCallback, useEffect, useMemo, useState } from "react"
const buttonStyle = { padding: "8px 16px", margin: "8px", borderRadius: "6px", backgroundColor: "#4CAF50",} as const;
export default function Sample() { const [counter, setCounter] = useState(0) const [incrementValue, setIncrementValue] = useState(1) const increment = useCallback((e) => { const increment = useMemo(() => function(e) { setCounter(x => x+incrementValue) }, [incrementValue]) useEffect(() => { const id = setTimeout(() => { setIncrementValue(2) console.log("Increment set to 2") }, 5000) return () => clearTimeout(id) }, []) return <div style={{height: "400px", width: "600px", margin: "auto", marginTop: "100px"}}> Current Value: {counter} <div> <button onMouseDown={(e) => { e.currentTarget.style.transform = "scale(0.95)"; e.currentTarget.style.boxShadow = "0 2px 4px rgba(0,0,0,0.2)"; }} onMouseUp={(e) => { e.currentTarget.style.transform = "scale(1)"; e.currentTarget.style.boxShadow = "0 2px 4px rgba(0,0,0,0.2)"; }} style={buttonStyle} onClick={increment}> Increment {incrementValue > 1 && `+${incrementValue}`} </button> </div> </div>}