Optimizing callbacks inside reusable React hooks
Tags: react, useeffect, usestate
Published: 2022-01-06 01:40:25

---
title: Optimizing callbacks inside reusable React hooks
published: true
description: Removing warnings about callbacks in your React.useEffect dependency array
tags: react, useEffect, useState
//cover_image: https://direct_url_to_image.jpg
---

### Problem 
You've created a custom react hook, useEventListener:

```js
const useEventListener = (type, callback) => {
  React.useEffect(() => {
    window.addEventListener(type, callback)
    return () => {
      window.removeEventListener(type, callback)
    }
  }, [])
}
```

Then you realize that you have missed the `type` and `callback` dependency, so you add them.

```js
const useEventListener = (type, callback) => {
  React.useEffect(() => {
    window.addEventListener(type, callback)
    return () => {
      window.removeEventListener(type, callback)
    }
  }, [type, callback])
}
```

Then you wonder to yourself, how often will this useEffect get run?

So you add a couple of console.logs detailing subscribe and unsubscribes.

```js
const useEventListener = (type, callback) => {
  React.useEffect(() => {
    console.log("subscribe")
    window.addEventListener(type, callback)
    return () => {
      console.log("unsubscribe")
      window.removeEventListener(type, callback)
    }
  }, [type, callback])
}
```

You also implement this hook in another file.

```js
function Simple() {
  useEventListener("resize", () => {
    console.log("hello")
  })
  return <div>hello</div>
}
```

This useEventListener will call your callback which log "hello" every time the browser resizes.

Also, subscribe will only get called one time.

[See it in action here](https://use-ref-callback.vercel.app/simple)

Sounds great, right? Well not so fast...

If you start adding things other than a console.log inside of your callback, then callback's memory address will start changing, and React will start running your `useEffect` in `useEventListener` a lot more than you expected it to.

Let's add a resize count to the resize event listener

```js
function ExternalExample() {
  const [count, setCount] = React.useState(0)
  useEventListener("resize", () => {
    setCount((prev) => prev + 1)
  })
  return (
    <div>
      <p>Count: {count}</p>
    </div>
  )
}
```

[See it in action here](https://use-ref-callback.vercel.app/external)

### Solution

So what do we do to solve this?

1. Wrap callback in a useCallback inside of our component
2. Remove callback from the useEffect
3. Wrap our callback in a ref

Option 1 is feasible for this use case, but as our code base grows, it's pretty annoying making all of your peers wrap their callbacks in useCallbacks, keep in mind, this callback approach needs to apply to *all* reusable hooks in our application.

Option 2 is not acceptable because the useEffect could be referencing *old* versions of callback when it's actually getting invoked. For this use case it's fine, but for other reusable hooks, it could have a stale callback.

Option 3 is our best bet!

Let's update useEventListener to store callback inside of a ref.

```js
const useEventListener = (type, callback) => {
  const callbackRef = React.useRef(null)

  React.useEffect(() => {
    console.log("assigning callback to refCallback")
    callbackRef.current = callback
  }, [callback])
  React.useEffect(() => {
    console.log("subscribe")
    window.addEventListener(type, refCallback.current)
    return () => {
      console.log("unsubscribe")
      window.removeEventListener(type, refCallback.current)
    }
  }, [type])
}
```

`callback` is still getting updated on every count update, but only the `useEffect` that's assigning `callback` is running. This is avoiding the event listener from subscribing and unsubscribing! We also don't have to add `refCallback.current` in the dependency array since updating refs do not trigger rerenders, which will not trigger a `useEffect` execution.

[See it in action here](https://use-ref-callback.vercel.app/ref-callback)

If you are happy with this approach as a reusable way to avoid adding callbacks inside of your `useEffect` dependency array, then feel free to stop here.


### Going the extra mile

In our code base, we have *lots* callbacks that get passed into reusable hooks.

Our useApi hook which interacts with external apis, accepts several callbacks: onSuccess, onError, api, and validate.

It gets pretty annoying writing this code:


```js
const onSuccessRef = React.useRef(null)
const onErrorRef = React.useRef(null)
const apiRef = React.useRef(null)
const validateRef = React.useRef(null)

React.useEffect(() => {
  onSuccessRef.current = onSuccess
}, [onSuccess])

React.useEffect(() => {
  onErrorRef.current = onError
}, [onError])

React.useEffect(() => {
  apiRef.current = api
}, [api])

React.useEffect(() => {
  validateRef.current = validate
}, [validate])
```

So with that... I'd like to introduce: `useCallbackRef`

Which turns this verbose code above into: 

```js
const onSuccessRef = useCallbackRef(onSuccess)
const onErrorRef = useCallbackRef(onError)
const apiRef = useCallbackRef(api)
const validateRef = useCallbackRef(validate)
```

`useCallbackRef` is written as follows:

```js
const useCallbackRef = (callback) => {
  const callbackRef = React.useRef(null)

  React.useEffect(() => {
    callbackRef.current = callback
  }, [callback])
  return callbackRef
}
```

But the problem with this approach is eslint will complain about `callbackRef`, it doesn't know that it's a ref!

To solve this, we need to patch eslint-plugin-react-hooks to let eslint know that our useCallbackRef returns stable values.

We need to install [patch-package](https://www.npmjs.com/package/patch-package) and [postinstall-postinstall](https://www.npmjs.com/package/postinstall-postinstall)

```bash
yarn add -D patch-package postinstall-postinstall
```

Once we have that installed, open up `node_modules/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js`

Go to line 907 where it has:
```js
if (name === 'useRef' && id.type === 'Identifier') {
```

And update that to be 

```js
if ((name === 'useRef' || 'useCallbackRef') && id.type === 'Identifier') {
```

Once that is updated, run patch-package:
```bash
node_modules/.bin/patch-package eslint-plugin-react-hooks
```

After that runs, you should have a patch file created in a patches folder, which contains the patch that will run on postinstall.

Add the following script in package.json:

`"postinstall": "patch-package"`

And now the warning in the dependency array is gone.

Long term it would be great if eslint-plugin-react-hooks was updated to support this functionality, but for now it doesn't, so that's why we're patching it. There is an open PR to add this functionality: https://github.com/facebook/react/pull/20513

You still have this warning from eslint:

>
ESLint: The ref value 'callbackRef.current' will likely have changed by the time this effect cleanup function runs. If this ref points to a node rendered by React, copy 'callbackRef.current' to a variable inside the effect, and use that variable in the cleanup function.(react-hooks/exhaustive-deps)
>

But that can be solved by assigning `callbackRef.current` to another variable such as `callback`.  You only have to do this when you're setting up subscriptions and unsubscribing from them in useEffects.

[See it in action here](https://use-ref-callback.vercel.app/use-callback-ref)



This is part one of this blog post, in the next part, I'll write about a custom eslint rule that marks the `callback` passed into `useCallbackRef` as "dirty", and it complains if you try invoke it.