DEV Community

Severin Ibarluzea
Severin Ibarluzea

Posted on

useCallback hook isn't a drop-in replacement for class methods, how to avoid rerenders and access state/props within useCallback

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)

Collapse
 
rajdeepdebnath profile image
Rajdeep Debnath

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?