Skip to content

Instantly share code, notes, and snippets.

@dfkaye
Last active June 6, 2022 19:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dfkaye/ed0824184b80c3293ae3a0ecceb6b983 to your computer and use it in GitHub Desktop.
Save dfkaye/ed0824184b80c3293ae3a0ecceb6b983 to your computer and use it in GitHub Desktop.
on the confusion around React's useEffect hook

// 6 June 2022

On the confusion around React's useEffect hook

It's June 2022 and there is a concerted effort underway to clarify the intention and behavior of ReactJS's useEffect() hook.

We find developers using it inside functional components as a handler for running other logic outside of the component's render step, but wondering repeatedly why it runs more than once at various times.

That is due to the original design of useEffect(), which in turn is due to the original class-based architecture of React components.

In the beginning...

React's original design leaned entirely on the class concept, embracing not-yet-implemented ES6 standards by several years, requiring the transpilation step with Babel.

What concerns us here is React's exposure of its internal lifecycle events.

  componentDidMount() {
    this.timerID = setInterval(
      () => this.tick(),
      1000
    );
  }

  componentWillUnmount() {
    clearInterval(this.timerID);
  }

Find the documentation at https://reactjs.org/docs/state-and-lifecycle.html.

First sign of trouble

From the examples, I think the componentDidMount, componentDidUpdate, and componentWillUnmount lifecycle methods should be renamed onMount, onUpdate, and onBeforeUnmount. You'll see why that matters shortly.

Then came hooks...

Hooks were introduced to support functional as opposed to object-oriented components. They replace the earlier lifecycle event handlers with injection functions.

The three listed motivations for them say more about the audience using React than about the React architecture:

  • Passing state between components is hard,
  • Complexity is hard,
  • Classes are confusing.

From documentation at https://reactjs.org/docs/hooks-intro.html.

Passing state between components can be done by publish-subscribe mechanism that React explicitly does not support. So, the first hook introduced is useState() which tries to solve the "passing state between component" problem. I think this is OK as it does not force an entire re-design of React itself.

The second hook introduced is useEffect(), a different mechanism for managing complexity, namely any interactions with other components, the React virtual DOM, and the lifecyle methods.

The Effect Hook, useEffect, adds the ability to perform side effects from a function component. It serves the same purpose as componentDidMount, componentDidUpdate, and componentWillUnmount in React classes, but unified into a single API.

From documentation at https://reactjs.org/docs/hooks-overview.html#effect-hook.

mental models, or when useEffect runs

Again, the componentDidMount, componentDidUpdate, and componentWillUnmount method names are less clear to me at least than event handler names like onMount, onUpdate, and onBeforeUnmount. These may have been so named to differentiate them from DOM Event Level 0 names (onEventName), which React encourages especially in JSX.

More serious is the telescoping of three different lifecycle events into a single hook, useEffect(). In my opinion this is the single mistake that demonstrates already that most users of React never really understood the architecture.

case: Only re-run the effect if count changes

Effects run when a component is mounted, before it is unmounted, and when it is updated, even if there is no change to the data.

In class components, such action logic must be specified.

componentDidUpdate(prevProps, prevState) {
  if (prevState.count !== this.state.count) {
    document.title = `You clicked ${this.state.count} times`;
  }
}

The Effect hook, on the other hand, always executes unless you specify something that can stop it.

If you make a network request to load default data into your functional component at mount time with useEffect(), that request will also happen every time the component is updated (re-rendered), and when it is un-mounted.

If that's not what you want, you have to pass an array of state variables as a second argument. The useEffect logic will execute only if those variable values change.

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // Only re-run the effect if count changes

From documentation at https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects.

Readers, slow down and pay attention

The usual population of UI developers, unfortunately - and yes, I used to be that young, too - generally do not read technical documentation to the end in order to understand or envision a fully working model. They read impatiently and stop when they find the answer, and proceed to copy+paste+modify the possible solution.

side-effects are bad

Calling them side-effects is bad. Side-effects are bad because they introduce gremlins into the system, i.e., they can reach out and change the global space. That is exactly counter to the idea of components.

React tries to eliminate the pub-sub pattern by using the virtual DOM to detect data changes and update the DOM only when data changes and only on data paths that have changed.

Hooks make that easier to specify - you don't need so much if-else in the functions you pass them.

Hooks make no distinction between the three render events in the component lifecycle - mount, update, and beforeUnmount. You the programmer have to make that distinction.

Different ways to think of all this

  • An Effect is not an Event but is triggered by one.
  • A State change is an Event that triggers an Update (render) using the changed state.
  • An Effect is triggered by an Update (render) event.
  • An Effect will complete if certain data conditions apply.
  • An Effect can be cancelled if certain data conditions do not apply.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment