89

I have this code:

const App: React.FC = () => {
  const [isOpen, setIsOpen] = React.useState(true);
  const [maxHeight, setMaxHeight] = React.useState();

  const wrapper = React.useRef<HTMLDivElement>(null);
  const content = React.useRef<HTMLDivElement>(null);

  const setElementMaxHeight = () => {
    if (content && content.current) {
      setMaxHeight(isOpen ? content.current.offsetHeight : 0);
    }
  };

  useEffect(() => {
   setElementMaxHeight();

    window.addEventListener("resize", setElementMaxHeight);

    return () => {
      window.removeEventListener("resize", setElementMaxHeight);
    };
  });

  const toggle = () => {
    setIsOpen(!isOpen);
  };

  return (
    <div>
      <button onClick={toggle}>
        <span className="nominal-result__expander fa" />
      </button>
      <div
        className="nominal-results__list-wrapper"
        ref={wrapper}
        style={!!maxHeight ? { maxHeight: `${maxHeight}px` } : undefined }
      >
        <div className="nominal-results__list" ref={content} />
      </div>
    </div>
  );
};

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

This will add and remove an event handler on each render.

Is this necessarily bad and does this actually gain anything from being a hook?

This came up in a code review and I am saying it is bad because it adds and removes the event listener on every render.

1 Answer 1

65

For this exact case you're right because undefined is passed as the dependencies of useEffect.

This means useEffect runs on every render and thus the event handlers will unnecessarily get detached and reattached on each render.

function listener() {
  console.log('click');
}

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

  window.React.useEffect(() => {

    console.log(`adding listener ${count}`);
    window.addEventListener("click", listener);

    return () => {
      console.log(`removing listener ${count}`);
      window.removeEventListener("click", listener);
    };
  }); // <-- because we're not passing anything here, we have an effect on each render
  
  window.React.useEffect(() => {
    setTimeout(() => {
      setCount(count + 1);
    }, 1000)
  });
  
  return count;
}

window.ReactDOM.render(window.React.createElement(Example), document.getElementById('root'))
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<div id="root"></div>

But if you explicitly declare no dependencies by passing in an empty array [], useEffect will only run once, thus making this pattern perfectly legitimate for event handler attachment.

function listener() {
  console.log('click');
}

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

  window.React.useEffect(() => {

    console.log(`adding listener ${count}`);
    window.addEventListener("click", listener);

    return () => {
      console.log(`removing listener ${count}`);
      window.removeEventListener("click", listener);
    };
  }, []); // <-- we can control for this effect to run only once during the lifetime of this component
  
  window.React.useEffect(() => {
    setTimeout(() => {
      setCount(count + 1);
    }, 1000)
  });
  
  return count;
}

window.ReactDOM.render(window.React.createElement(Example), document.getElementById('root'))
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<div id="root"></div>

Sign up to request clarification or add additional context in comments.

6 Comments

"..the event handlers will unnecessarily get detached and reattached on each render.". Is it the same as if we wrote whatever piece of code without any hook. E.g., assume we have assignment (const var = "something"), on each render we'll have that assignment work as if we have useEffect(() => const var = "something"). Please do not consider variable scope
So why would you ever use a useEffect without a dependency array?
@dwjohnston well, you'd use it for any operation that needs to run on every render. What operation that is depends on the app you're building.
@nem in that case, couldn't you just declare that functionality in the function, without using useEffect?
It depends on the use case. A sync function call within the render cycle will run within the render, and it might affect perf. An effect always runs after the render, so at the very least, is less detrimental to the UX since the screen is already showing what it should show by the time the effect starts running.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.