Some insight on React behavior would be appreciated. Issue with custom hook

Advertisements

I have a small react example that is puzzling me.

You can run the code at codesanbox

The code is very simple, there is a component TestComponent that displays ‘wowzers’, and calls a custom hook useCustomHook.

useCustomHook does three things. It declares a useState [list, set_list], then a useEffect based on list to inform when there is a change, and then a 1 second timer that will update the list.

What I expected was that the useEffect would initially run and then a second later it would run again when the list is updated. However, it updates every second. Not only this, but the useCustomHook is being re-entered from TestComponent starting the process all over again, and this happens repeatedly.

Can someone explain please why, when the list is updated with set_list in the timer callback that this causes TestComponent to call useCustomHook again (and again)

I thought I understood that principles of using react and have developed numerous but small applications. This simple example is really throwing me off. Any help would be appreciated.

The code in index.js is a follows.

import { useState, useEffect } from "react";
import React from "react";
import ReactDOM from "react-dom";

const useCustomHook = () => {
  const [list, set_list] = useState([]);
  useEffect(() => console.log("useEffect", "list updated"), [list]);
  setTimeout(() => set_list(() => []), 1000);
};

const TestComponent = () => {
  const hook = useCustomHook();
  return <span>wowzers</span>;
};

class App extends React.Component {
  render() {
    return <TestComponent />;
  }
}

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

>Solution :

setTimeout is right in the hook body… each time the hook is called it runs the entire body. The timeout is enqueueing a state update which triggers a rerender. Rerenders run all the hooks again.

const useCustomHook = () => {
  const [list, set_list] = useState([]);

  useEffect(() => console.log("useEffect", "list updated"), [list]);

  setTimeout(() => set_list(() => []), 1000); // <-- unintentional side-effect!
};

What I expected was that the useEffect would initially run and then a
second later it would run again when the list is updated.

Place the setTimeout in a mounting useEffect, i.e. empty dependency array, so it runs exactly once after the initial mounting render cycle.

const useCustomHook = () => {
  const [list, set_list] = useState([]);

  useEffect(() => console.log("useEffect", "list updated"), [list]);

  useEffect(() => {
    setTimeout(() => set_list(() => []), 1000);
  }, []);
};

Leave a ReplyCancel reply