useCallback hook isn't a drop-in replacement for class methods, how to avoid rerenders and access state/props within useCallback
Severin Ibarluzea
Posted on January 5, 2019
React hooks are really cool. I'm was converting some libraries over to hooks when I ran into a major performance snag.
At first glance, the following components might look like they do the same thing...
// Class Style
class ClassStyleComponent extends React.Component {
state = { val: 0 }
onAdd = () => {
const { val } = this.state
this.setState({ val: val + 1 })
}
onSubtract = () => {
const { val } = this.state
this.setState({ val: val - 1 })
}
render() {
const { val } = this.state
return (
<div>
<div>val: {val}</div>
<button onClick={this.onAdd}>
Increment
</button>
<button onClick={this.onSubtract}>
Multiply by 2
</button>
</div>
)
}
}
// Hooks Style
const NaiveHooksComponent = () => {
const [val, changeVal] = useState(0)
const onAdd = useCallback(() => changeVal(val + 1), [val])
const onSubtract = useCallback(() => changeVal(val - 1), [val])
return (
<div>
<div>val: {val}</div>
<button onClick={onAdd}>
Increment
</button>
<button onClick={onSubtract}>
Multiply by 2
</button>
</div>
)
}
Sure enough, these components functionally do the same thing, but there's a critical performance difference.
The buttons are rerendered every time val
changes on the hooks-style component, but in the class-style component, the buttons are only rendered once!
The reason for this is useCallback
must recreate the callback function every time the state changes. The class component callbacks access state without creating a new function.
Here's the easy fix: Leverage useReducer
and use the state passed to the reducer.
Here's the hooks component rewritten such that the buttons only render once:
const ReducerHooksComponent = () => {
const [val, incVal] = useReducer((val, delta) => val + delta, 0)
const onAdd = useCallback(() => incVal(1), [])
const onSubtract = useCallback(() => incVal(-1), [])
return (
<div>
<div>val: {val}</div>
<button onClick={onAdd}>
Increment
</button>
<button onClick={onSubtract}>
Multiply by 2
</button>
</div>
</div>
)
}
All fixed! The buttons only render once now because onAdd
and onSubtract
don't change every time val
changes. You can adapt this to more complex use cases by passing more detailed actions.
There's a slightly more complex technique by sophiebits that works great for event callbacks. To use it, we'll have to define a custom hook called useEventCallback
.
function useEventCallback(fn) {
let ref = useRef()
useLayoutEffect(() => {
ref.current = fn
})
return useCallback((...args) => (0, ref.current)(...args), [])
}
// This looks a lot like our intuitive NaiveHooksComponent!
const HooksComponentWithEventCallbacks = () => {
const [val, changeVal] = useState(0)
// Swap useCallback for useEventCallback
const onAdd = useEventCallback(() => changeVal(val + 1))
const onSubtract = useEventCallback(() => changeVal(val - 1))
return (
<div>
<div>val: {val}</div>
<button onClick={onAdd}>
Increment
</button>
<button onClick={onSubtract}>
Multiply by 2
</button>
</div>
)
}
This example is trivial (buttons don't have a huge rendering cost), but bad memoization can have massive performance implications when refactoring a large application.
Cheers and best of luck adopting hooks!
Posted on January 5, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 28, 2024
November 27, 2024