javascriptreactjsreact-hooks

The state keeps updating via useEffect, while there is no change in state


In the DisplayName component the state keeps updating even though there is no change in state. console.log("Rendered") and console.log(value) keeps printing. I can't understand, can someone help me on this.

Below is the code:

import React, { useRef, useState, useEffect, useMemo } from "react";
import './App.css';
import FComponent from "./FComponent";

function App() {
  const [name, setName] = useState("am");
  const [counter, setCounter] = useState(1);

  const result = useMemo(() => {
    return factorial(counter)
  }, [counter])

  console.log("result")

  const displayName = () => {
    return {name};
  }

  return (
    <div className="App">
      <h1>Factorial of {counter} is: {result}
      </h1>
      <div>
        <button onClick={() => setCounter(counter - 1)}>Decrement</button>
        <button onClick={() => setCounter(counter + 1)}>Increment</button>
      </div>
      <hr></hr>
      <div>
        <div>
          <label>Enter Name</label>
        </div>
        <input
          type="text"
          value={name}
          onChange={(e) => setName(e.target.value)}
        >
        </input>
        <hr></hr>
        <DisplayName displayName={displayName} />
      </div>
    </div>
  );
}

const DisplayName = ({ displayName }) => {
  console.log("Rendered")

  const [value, setValue] = useState("");
  console.log(value);

  useEffect(() => {
    setValue(displayName());
  })
  return (<p>{`My name is ${value}`}</p>)
}

function factorial(n) {
  if (n < 0) {
    return -1
  }
  if (n === 0) {
    return 1
  }
  return n * factorial(n - 1)
}

function ten() {
  return new Date().toString()
}

export default App;

I expected that in DisplayName component, the state named value should only update once via the useEffect from "" to {"am"}. Now once it's updated to {"am"} it should stop updating.


Solution

  • Issues

    1. The displayName callback is redeclared each render cycle in the parent component and passed down as props. Since this is a new function reference value it triggers the child component to re-render.
    2. The useEffect hook in the child component is missing a dependency array, so the effect callback runs each render cycle and unconditionally enqueues a React state update, which again triggers another component render cycle.
    3. The displayName function returns an object instead of a string value. This means that it's a new object reference each time the state update is enqueued and React can't bail out of the state update since the reconciliation process uses shallow reference equality checks. The name state invariant isn't maintained, i.e. it is defined as a string, it should always be a string. String values are always self-equal, regardless of reference equality.

    Solution Suggestion

    function App() {
      const [name, setName] = useState("am"); // <-- string type
    
      ...
    
      const displayName = useCallback(() => {
        return name; // <-- return string type value
      }, [name]);
    
      return (
        <div className="App">
          ...
          <div>
            <div>
              <label>Enter Name</label>
            </div>
            <input
              type="text"
              value={name}
              onChange={(e) => setName(e.target.value)}
            />
            <hr />
            <DisplayName displayName={displayName} />
          </div>
        </div>
      );
    }
    
    const DisplayName = ({ displayName }) => {
      const [value, setValue] = useState("");
    
      useEffect(() => {
        setValue(displayName());
      }, [displayName]); // <-- prop dependency
    
      return <p>My name is {value}</p>;
    };
    

    If you simply only want to "seed" the local value state then omit the displayName as a dependency. Using an empty dependency will run the effect only once after the initial render cycle.

    const DisplayName = ({ displayName }) => {
      const [value, setValue] = useState("");
    
      useEffect(() => {
        setValue(displayName());
      }, []); // <-- empty dependency
    
      return <p>My name is {value}</p>;
    };
    

    Suggested Improvement

    It's a common general React anti-pattern to store passed prop values into local state, e.g. passing displayName to copy the name state value from the parent component to the child components. Update the DisplayName component to just consume the name value you wish to render directly.

    function App() {
      const [name, setName] = useState("am");
    
      ...
    
      return (
        <div className="App">
          ...
          <div>
            <div>
              <label>Enter Name</label>
            </div>
            <input
              type="text"
              value={name}
              onChange={(e) => setName(e.target.value)}
            />
            <hr />
            <DisplayName displayName={name} />
          </div>
        </div>
      );
    }
    
    const DisplayName = ({ displayName }) => {
      return <p>My name is {displayName}</p>;
    };