Trouble with useEffect running every render? `useEffectRef` to the rescue!
Tags: javascript, react, reacthooks, advanced
Published: 2021-01-21 06:49:05

---
title: Trouble with useEffect running every render? `useEffectRef` to the rescue!
published: true
description: This article includes a helpful tip and example for when you have a react effect that's running too often. Usually a callback prop.
tags: javascript, react, reacthooks, advanced
//cover_image: https://direct_url_to_image.jpg
---

## The problem

Here's the standard contrived `Counter` component, except I've added an `onChange` prop, so that the parent component can listen to when the count is updated.

```js
function Counter({ onChange }) {
	const [count, setCount] = useState(0)

	useEffect(() => {
		onChange(count)
	}, [count, onChange])

	return (
		<>
			<p>{count}</p>
			<button onClick={() => setCount((c) => c + 1)}>+</button>
		</>
	)
}
```

If you use the `react-hooks` eslint rule, which is built into Create React App, you'll see that it tells you to add `onChange` _and_ `count` to the dependency array.

Usually, the eslint rule is right, and abiding by it will help prevent bugs. But in practice, this can cause the effect to run on _every_ render.

```js
// every render, this callback function is a new, fresh value
// if a state update happens here, or higher up,
// the effect in `Counter` will run,
// and this alert gets called
// ...every update
<Counter onChange={(newCount) => alert(`new count: ${newCount}`)} />
```

No good! We only want to listen to changes, not all updates! 🙃

## The solution

> Before I continue, **consider this a last resort**. If you use this for lots of values, then your effects will miss some important updates, and you'll end up with a stale UI. Reserve this for things that change every render, which are usually callback props. For objects, [this might work a lot better](https://github.com/discord/use-memo-value).

Anyway, here's my preferred solution, which I feel aligns well with [the intended mindset of hooks](https://twitter.com/ryanflorence/status/1125041041063665666).

```js
import { useState, useEffect, useRef } from "react"

function Counter({ onChange }) {
	const [count, setCount] = useState(0)

	const onChangeRef = useRef(onChange)
	useEffect(() => {
		onChangeRef.current = onChange
	})

	useEffect(() => {
		onChangeRef.current(count)
	}, [count, onChangeRef])

	return (
		<>
			<p>{count}</p>
			<button onClick={() => setCount((c) => c + 1)}>+</button>
		</>
	)
}
```

This works because refs have free floating, mutable values. They can be changed without causing re-renders, and aren't a part of the reactive flow, like state and props are.

Effects run from top to bottom in the component. The first effect runs and updates `onChangeRef.current` to whatever callback we've been passed down. Then the second effect runs, and calls it.

You can package the above in a custom hook for reuse. It comes in handy, especially for callback props.

```js
import { useState, useEffect, useRef } from "react"

function Counter({ onChange }) {
	const [count, setCount] = useState(0)

	const onChangeRef = useEffectRef(onChange)
	useEffect(() => {
		onChangeRef.current(count)
	}, [count, onChangeRef])

	return (
		<>
			<p>{count}</p>
			<button onClick={() => setCount((c) => c + 1)}>+</button>
		</>
	)
}

function useEffectRef(value) {
	const ref = useRef(value)
	useEffect(() => {
		ref.current = value
	})
	return ref
}
```

Note: the ESLint rule will tell you to add `onChangeRef` to the effect dependencies. _Any_ component-scoped value used in an effect should be a dependency. Adding it isn't a problem in practice; it doesn't change, so it won't trigger re-renders.

## Alternatives

### Call the callback prop while updating the value

```js
function Counter({ onChange }) {
	const [count, setCount] = useState(0)

	const handleClick = () => {
		setCount((c) => c + 1)
		onChange(c + 1)
	}

	return (
		<>
			<p>{count}</p>
			<button onClick={handleClick}>+</button>
		</>
	)
}
```

This works well in this contrived example, and this may even be better for your case!

However, let's say we add a minus button to this component. Then we have to remember to call the callback when that's clicked as well, and for any other potential case it updates. That, and notice we have to put the update logic twice (`c + 1`), due to the use of the callback prop. This is somewhat error-prone.

I find an effect is more future proof, and more clearly conveys the intent of "call `onChange` whenever count changes".

However, this path _does_ let you avoid mucking around with `refs`, so it still makes for a good alternative. Just giving one more potential tool in the toolbox 🛠

### `useCallback` on the parent

```js
const handleChange = useCallback((count) => {
  alert(count)
}, [])

<Counter onChange={handleChange} />
```

This works, and is probably the "most correct" solution, but having to `useCallback` every time you want to pass a callback prop is unergonomic, and easy to forget.

### `// eslint-disable-line`

This could cause future bugs if you need to add a new dependency and forget to. The rule is rarely wrong in practice, only ever if you're doing something weird, like a custom dependency array.