`useEffect` doesn't run after the first `render` and whenever its dependencies change

Photo by WrongTog on Unsplash

`useEffect` doesn't run after the first `render` and whenever its dependencies change

Because wording matters

·

3 min read

Do you know useEffect?

useEffect(() => {
  console.log(value)
}, [value])

This effect runs after the first render and whenever value changes.

I bet you've probably learned it that way.

This effect runs after the first render...

If you've read my article about React hooks' flow, you probably know that statement is inaccurate: useEffects run after the browser is painted, which happens not only after the render but also:

  • after React updates the DOM, and
  • after running the layout effects.

Note we don't mention effect clean-ups since we're speaking about the first render here. And, besides, the render itself happens after React runs the lazy state initializers.

So the first half of the statement is nuanced, but nuances matter. If you're memorizing "useEffect runs after renders" you won't be lying to yourself, but you'll only be telling part of the story. "useEffect runs after the browser has painted the component" tells much more. If you were to memorize one of the two, go for the second one.

But what if I told you the second bit is also wrong?

...and whenever value changes.

Code speaks for us:

const Component = () => {
  let value = 0

  useEffect(() => {
    console.log('useEffect', value)
  }, [value])

  return (
    <button
      onClick={() => {
        value++
        console.log('onClick', value)
      }}
    >
      Update x
    </button>
  );
}

Step by step:

  1. Render Component, thus "useEffect", 0 will be logged when the effect runs after the browser is painted.
  2. Click on the Update x button.
  3. "onClick", 1 will be logged.
  4. And, according to the definition, since value has changed from 0 to 1: "useEffect", 1 will be logged.

But it won't. Instead, nothing else will happen. An effect doesn't run after one of its dependencies change. An effect runs once your component rerenders and one of its dependencies have changed. The first affirmation is only true if its dependencies are state (or derived from state) and updated according to React's paradigm. Don't trust me, trust the code and play with it!

What are effects, anyway?

  • React components are pure. effects are the place where side-effects are meant to happen. Whatever that means. It's fine if that sounds weird. We won't dive into it today.
  • effects synchronize the state of your app with the outside world (quoting Kent C. Dodds here).

Here, state is the important nuance. state updates (if performed according to React's model) produce new render cycles. New render cycles give a chance to effects to run if any of their dependencies are different. useEffect is your little state synchronization machine.

Rephrasing it

useEffect(() => {
  console.log(value)
}, [value])

This effect runs:

  • after this component is painted in the browser for the first time*, and
  • on subsequent component rerenders, if value has changed.
  • Actually, whenever React acknowledges a particular instance of a component for the first time, after running its lazy state initializers, rendering it, updating the DOM, running its layout effects and painting those DOM updates.

Or, as Ryan Florence once put it:

The question is not "when does this effect run" the question is "with which state does this effect synchronize with"

useEffect(fn) // all state

useEffect(fn, []) // no state

useEffect(fn, [these, states])

Because, at the end of the day, state changes are what produce rerenders, and a rerender is the only way we can give our effects a chance to run.