useCallback hook isn't a drop-in replacement for class methods, how to avoid rerenders and access state/props within useCallback
Tags: react, javascript
Published: 2019-01-05 03:09:46

---
title: useCallback hook isn't a drop-in replacement for class methods, how to avoid rerenders and access state/props within useCallback
published: true
description: If you're switching from React components to the new hooks style, you're going to run into a big performance issue. useCallback is not quite the same as having a class method because the callback cannot access state. Here's how to avoid slowing down your app. 
tags: #react #javascript
---

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...


```javascript

// 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:

```javascript
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](https://github.com/facebook/react/issues/14099#issuecomment-440013892) that works great for event callbacks. To use it, we'll have to define a custom hook called `useEventCallback`.

```javascript

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!