Swizec Teller - a geek with a hatswizec.com

Senior Mindset Book

Get promoted, earn a bigger salary, work for top companies

Senior Engineer Mindset cover
Learn more

    useCallback is a code smell

    One of my favorite ways to simplify and de-gotcha React code is to rip out all the useCallback and useMemo drama. 90% of the time you don't need it.

    What useCallback is for

    useCallback and friends are React hooks that help you create a stable reference to a JavaScript object, which helps React's rendering engine understand the object hasn't changed.

    For example:

    function FidgetSpinner() {
    	const [spinning, setSpinning] = useState(false)
    
    	const newFuncEveryTime = () => {
    		setSpinning(!spinning)
    	}
    
    	const stableFunc = useCallback(() => {
    		setSpinning(!spinning)
    	}, [spinning])
    
    	return (
    		<>
    			<p>Is it spinning? {spinning}</p>
    			<Spinner spinning={spinning} onClick={...} />
    		</>
    	)
    }
    

    Assume <Spinner> renders a fidget spinner that's either spinning or not. The onClick prop accepts a function that updates the spinning state.

    You can't move that state into <Spinner> because you want to show the description. You need to handle onClick inside the <Spinner> because that's what renders DOM elements.

    Referential stability and re-renders

    React uses prop values to decide when to re-render the <Spinner> component. When values change, components re-render.

    For functions and other objects that "value" is their memory address. Known as a reference. Even if the function or object looks the same, but lives in a new address, React thinks it's different and re-renders your component.

    That's where useCallback comes in.

    const newFuncEveryTime = () => {
      setSpinning(!spinning)
    }
    

    This is a new function any time React's engine touches the <FidgetSpinner> component (calls the function). Whether it updates the DOM or not, calling the component re-defines this function with a new memory address. That causes a re-render of <Spinner>.

    const stableFunc = useCallback(() => {
      setSpinning(!spinning)
    }, [spinning])
    

    This creates a memoized function with a stable memory address. It only creates a new function when the dependency array changes. In this case any time spinning's value changes, useCallback re-instantiates your function with a new address.

    useCallback gotchas

    There is a tricky potential bug in my code above 👉 the wrong dependency array creates a good old JavaScript closure problem.

    Define it like this:

    const stableFunc = useCallback(() => {
      setSpinning(!spinning)
    }, [])
    

    And the spinning value is baked into the function "forever". Calling this function won't toggle spinning from false to true and back as you'd expect, it's always going to set it to true. Or false if the initial value was true.

    That's a big gotcha for dubious performance benefits.

    Why engineers useCallback

    A reply likened it to "using sunscreen at night" 🤣

    There's 2 understandable reasons I've seen:

    1. Engineer is worried about performance and wants to help. Admirable goal! But useCallback and friends introduce a memory overhead. JavaScript machinery needs to keep a stack of all those memoized functions and lug it around wherever a component goes. Do it too much or get it subtly wrong and this leads to fun memory leaks and stale renders. Big problem in ye olden days of hand-rolled "frameworks".
    2. Engineer encounters an infinite loop. This one sucks. It happens when you use an unstable callback or object as a useEffect dependency. Every render re-defines the callback, triggers the effect, causes a re-render, which ... 😬

    And there's a third version I've seen that ... why??

    function Component() {
      const SubComponent = useCallback(() => {
        return <div>This is a component damn it!</div>
      }, [])
    
      return (
        <>
          <p>Lorem Ipsum</p>
          {SubComponent()}
        </>
      )
    }
    

    Please don't do that. Define a component and let React do its thing. 🥺

    How you can avoid useCallback

    Best way to avoid useCallback is by moving your functions out of component scope. Use pure functions that depend fully on passed-in arguments instead of values in scope.

    Example 1

    A better version of the example above looks like this:

    function FidgetSpinner() {
      const [spinning, setSpinning] = useState(false)
    
      return (
        <>
          <p>Is it spinning? {spinning}</p>
          <Spinner spinning={spinning} setSpinning={setSpinning} />
        </>
      )
    }
    

    setSpinning is a stable function! You can pass it into <Spinner>, which can use its alternative form to toggle state:

    setSpinning((spinning) => !spinning)
    

    You can call React setters with a function that gets current value as its argument.

    Example 2

    Another common opportunity is turning functions using local scope into independent (testable 🤘) functions. Like when building forms with react-hook-form

    function ComplicatedStuff() {
      const formMethods = useForm()
    
      const fieldValue = formMethods.watch("field")
    
      return (
        <>
          <p>Live current value of field: {fieldValue}</p>
          <FormRenderComponent onSubmit={onSubmit} />
        </>
      )
    }
    

    formMethods.watch watches your input field and returns its current value. Great when you're building dynamic forms.

    The temptation is to then write the onSubmit function like this:

    function ComplicatedStuff() {
    	const formMethods = useForm()
    
    	const fieldValue = formMethods.watch('field')
    
    	async function onSubmit() {
    		await fetch('...', {
    			method: 'POST',
    			body: JSON.stringify({
    				fieldValue
    			})
    		})
    	}
    

    Then you think "Oh no, performance!" and memoize that function with a useCallback. Run into staleness issues and add all the fields into its dependency array.

    Now you have unnecessary full re-renders on every keypress 💩

    Instead, try this:

    async function onSubmit(values) {
    	await fetch('...', {
    		method: 'POST',
    		body: JSON.stringify({
    			fieldValue: values.fieldValue
    		})
    	})
    }
    
    function ComplicatedStuff() {
    	const formMethods = useForm()
    
    	const fieldValue = formMethods.watch('field')
    

    React-hook-form passes all current values into the onSubmit function. You don't need to rely on component scope!

    Cough

    Sure, that's how this library works, my point is the general principle 👉 look for opportunities to refactor your functions so they rely on arguments instead of scope.

    Then you can move them anywhere. Flexibility 🤩 (and stable references)

    When do you need useCallback and friends?

    If you're writing a library or core piece of functionality that's going to show up in lots of components, memoize all the things.

    6tmch3jpgc6dc4j

    A library like React Query or useAuth can make your whole app re-render by accident. You do want to prevent that.

    A callback shared in 3 small components? Meh. Focus on keeping renders fast instead of worrying about re-renders.

    Cheers,
    ~Swizec

    PS: computers are fast these days and in my experiments with React 18 it took thousands of elements to notice performance issues

    PPS: I hear rumors the React team is working on an auto-memoizing engine that handles all this for you 🤞

    Published on September 16th, 2022 in Technical, React, Frontend, Programming Lessons

    Did you enjoy this article?

    Continue reading about useCallback is a code smell

    Semantically similar articles hand-picked by GPT-4

    Senior Mindset Book

    Get promoted, earn a bigger salary, work for top companies

    Learn more

    Have a burning question that you think I can answer? Hit me up on twitter and I'll do my best.

    Who am I and who do I help? I'm Swizec Teller and I turn coders into engineers with "Raw and honest from the heart!" writing. No bullshit. Real insights into the career and skills of a modern software engineer.

    Want to become a true senior engineer? Take ownership, have autonomy, and be a force multiplier on your team. The Senior Engineer Mindset ebook can help 👉 swizec.com/senior-mindset. These are the shifts in mindset that unlocked my career.

    Curious about Serverless and the modern backend? Check out Serverless Handbook, for frontend engineers 👉 ServerlessHandbook.dev

    Want to Stop copy pasting D3 examples and create data visualizations of your own? Learn how to build scalable dataviz React components your whole team can understand with React for Data Visualization

    Want to get my best emails on JavaScript, React, Serverless, Fullstack Web, or Indie Hacking? Check out swizec.com/collections

    Did someone amazing share this letter with you? Wonderful! You can sign up for my weekly letters for software engineers on their path to greatness, here: swizec.com/blog

    Want to brush up on your modern JavaScript syntax? Check out my interactive cheatsheet: es6cheatsheet.com

    By the way, just in case no one has told you it yet today: I love and appreciate you for who you are ❤️

    Created by Swizec with ❤️