Search code examples
javascriptreactjsreact-hooksuse-effect

React: Difference between side effect in component's function vs effect?


I'm trying to understand the practical difference between having a side effect in a component function, vs having it within an effect which has no dependency array passed in (and as such should fire on every render). From what I can observe, they both run at the same frequency. I realize that effects allow for cleanup at the proper time, but I'm simply curious about the scenario where cleanup is not a factor.

The following CodePen shows what I'm talking about.

https://codepen.io/benrhere/pen/GRyvXZZ

The important piece is:

function EffectVsFunctionQuestion() {
  const [count, setCount] = React.useState(0);

  React.useEffect(()=>{
    console.log("incremented within hook")
  });
  console.log("incremented within component function")
...
}

Solution

  • Side-effects issued from the useEffect hook have the benefit of being triggered at most once per render cycle. The render cycle here means the "commit phase" when React has computed the next view and commits it to the DOM.

    This should not be confused with what React calls the "render phase" when it renders out the component (and children and entire ReactTree) to compute what changed and needs to be committed to the DOM.

    enter image description here

    The entire function body of a React function component is the "render" function. As you can see in the diagram, any unintentional side-effects in the render or body will occur during the "render phase" which can can be paused, aborted, or restarted (i.e. run again) by React. Note that the "render phase" is to be pure and free from side-effects.

    The "commit phase" the component can work with the DOM and run side-effects.

    Why does this matter? How to tell the difference.?

    React actually ships with a StrictMode component that helps you detect unexpected side-effects.

    Detecting unexpected side effects

    Strict mode can’t automatically detect side effects for you, but it can help you spot them by making them a little more deterministic. This is done by intentionally double-invoking the following functions:

    • Class component constructor, render, and shouldComponentUpdate methods
    • Class component static getDerivedStateFromProps method
    • Function component bodies
    • State updater functions (the first argument to setState)
    • Functions passed to useState, useMemo, or useReducer

    Here's an example sandbox demo that demonstrates the unexpected side effects.

    Edit react-difference-between-side-effect-in-components-function-vs-effect

    Note that the unexpected effect is doubled.

    enter image description here

    Code:

    const externalValue1 = { count: 0 };
    const externalValue2 = { count: 0 };
    
    function EffectVsFunctionQuestion() {
      const [state, setState] = React.useState(0);
    
      React.useEffect(() => {
        externalValue1.count++;
        console.log("incremented within hook", externalValue1.count);
      });
      externalValue2.count++;
      console.log("incremented within component function", externalValue2.count);
    
      return (
        <button type="button" onClick={() => setState((c) => c + 1)}>
          Render
        </button>
      );
    }