Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React useState with an empty object causes an infinite loop

Using React hooks with a child component that should get the initial state from the parent and update the parent on every internal state change.
I figured that since it's always the same reference the useEffect of the child should not get called infinitely.

If the initial state of the child is an empty object I get an infinite loop.
If the initial state of the child is taken from the props it works great.

Not sure what's causing it.
You can change the first useState inside the child component to an empty object to make the infinite loop start.

Please review the sandbox below:
https://codesandbox.io/s/weird-initial-state-xi5iy?fontsize=14&hidenavigation=1&theme=dark
Note: I've added a counter to the sandbox to stop the loop after 10 runs and not crash the browser.

import React, { useState, useEffect, useCallback } from "react";

const problematicInitialState = {};

/* CHILD COMPONENT */
const Child = ({ onChange, initialData }) => {
  const [data, setData] = useState(initialData); // if initialData is {} (a.k.a problematicInitialState const) we have an infinite loop

  useEffect(() => {
    setData(initialData);
  }, [initialData]);

  useEffect(() => {
    onChange(data);
  }, [data, onChange]);

  return <div>Counter is: {data.counter}</div>;
};

/* PARENT COMPONENT */
export default function App() {
  const [counterData, setCounterData] = useState({ counter: 4 });

  const onChildChange = useCallback(
    (data) => {
      setCounterData(data);
    },
    [setCounterData]
  );

  return (
    <div className="App">
      <Child onChange={onChildChange} initialData={counterData} />
    </div>
  );
}
like image 349
Loves2Develop Avatar asked Oct 19 '25 05:10

Loves2Develop


2 Answers

How about putting the state only in the parent component instead, and have the child only reference the props passed down to it, without any state of its own?

const Child = ({ counterData, setCounterData }) => {
  return (
    <div>
      <div>Counter is: {counterData.counter}</div>
      <button
        onClick={() => setCounterData({ counter: counterData.counter + 1 })}
      >increment</button>
    </div>
  );
};

const App = () => {
  const [counterData, setCounterData] = React.useState({ counter: 4 });
  return (
    <div className="App">
      <Child {...{ counterData, setCounterData }} />
    </div>
  );
}

ReactDOM.render(<App />, document.querySelector('.react'));
<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 class="react"></div>
like image 104
CertainPerformance Avatar answered Oct 21 '25 19:10

CertainPerformance


Problem is that in JS {} !== {} because objects, unlike primitive values, are compared by reference, not value.

In you useEffect you're comparing 2 objects, because they always have different reference, the'll never be the same in JS land and your useEffect will trigger, setting new object and you got yourself an infinite loop.

You shouldn't use hooks in the same way you used class components in react, meaning you should do

const [counter, setCounter] = useState(4);

This way, you'll pass primitive value down to your child component and useEffect will have much more predictable behaviour.

Also, while this is a test case, you should rarely (read: never) try to set child sate to parent state. You already pass that data from parent to child, no need to create redundant state in your child component, just use the passed in data.

like image 20
fila90 Avatar answered Oct 21 '25 18:10

fila90



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!