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!
Top comments (1)
Hi Severin,
Thank you for this article. But as I was trying to practice this one I found that even without useReducer the functional component is not rendering on every change of val. Any thoughts on this?