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.
Honestly at this point useCallback is a code smell
— Swizec Teller (@Swizec) September 14, 2022
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
It’s not that people are using it wrong, it’s that they’re using useCallback but don’t need to
— Swizec Teller (@Swizec) September 14, 2022
Outside of very specific contexts the overhead actually makes your code slower (larger context for JS to schlep around)https://t.co/ztXnwl9yRi
A reply likened it to "using sunscreen at night" 🤣
There's 2 understandable reasons I've seen:
- 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". - 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
90% of useCallback I’ve seen in the wild wouldve been better solved by
— Swizec Teller (@Swizec) September 14, 2022
a) a clean function based on args defined outside your component
b) not leaking scope into unrelated components
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.
I mean what your function does should depend on its arguments, not the state of its context pic.twitter.com/tKUAxBdCtq
— Swizec Teller (@Swizec) September 14, 2022
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.
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 🤞
Continue reading about useCallback is a code smell
Semantically similar articles hand-picked by GPT-4
- Fixing laggy UI with React.memo
- How React Query gives you almost everything you thought you needed GraphQL for
- React hooks in a nut shell 🥐
- Update state during render, better explained
- Custom react hooks ❤️
Learned something new? Want to become a React expert?
Learning from tutorials is easy. You follow some steps, learn a smol lesson, and feel like you got dis 💪
Then comes the interview, a real world problem, or a question from the boss. Your mind goes blank. Shit, how does this work again ...
Happens to everyone. Building is harder than recognizing and the real world is a mess! Nothing fits in neat little boxes you learned about in tutorials.
That's where my emails come in – lessons from experience. Building production software :)
Leave your email and get the React email Series - a series of curated essays on building with React. Borne from experience, seasoned with over 12+ years of hands-on practice building the engineering side of growing SaaS companies.
Get Curated React Essays
Get a series of curated essays on React. Lessons and insights from building software for production. No bullshit.
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 ❤️