Home > Software design >  React updating a components state from another component causes infinite loop
React updating a components state from another component causes infinite loop

Time:01-06

I have what is basically a form wizard with multiple steps. The wizard is divided into two parts: labels and contents. When a content component changes its internal state, from say incomplete to error or something, I want the wizard to update the label. The issue I am running into is grabbing the state from the contents component, trying to save that in the wizard component as a state so I can update the labels is causing an infinite loop.

While I understand the problem I don't really know how to solve it. This is a very minimal example and my real components use some advanced features like cloneElement to pass props to user components without them having to worry about setting 10 different props. So far this has worked flawlessly until now.

So I understand that each time I update my main components state, it's going to re-render the children, which will call the same set state function forever. What can I do instead?

import React from "react";

import "./styles.css";

// This component containsLabel children
interface LabelState {
  error: boolean;
}
interface LabelGroupProps {
  states: LabelState[],
}
const LabelGroup = (props: LabelGroupProps) => {
  return (<div>I am Comp A</div>)
}

// This component contains Component children
interface ContentState {
  error: boolean;
}
interface ContentGroupProps {
  getStates: (state: ContentState, index: number) => void
}
const ContentGroup = (props: ContentGroupProps) => {
  // Indicate that step 2 has an error
  props.getStates({
    error: true
  }, 2)
  return (<div>I am Comp B</div>)
}

// This is the main wizard that contains both above components
// When the state of the a content component changes, the labels must be updated.
const App = () => {
  const [states, setStates] = React.useState<LabelState[]>([]);

  const getStates = (state: ContentState, index: number) => {
    // This causes an infinite loop
    // The intention is to save this state and then update the labels
    const temp = [];
    temp[index] = state;
    setStates(prev => [...prev, ...temp])
  }

  return (

    <div className="App">
      <LabelGroup states={states}/>
      <ContentGroup getStates={getStates}/>
    </div>
  );
};

export default App;

https://codesandbox.io/s/react-fiddle-forked-1ypi6

CodePudding user response:

Your issue is caused by updating state every time ContentGroup renders. Any time there is a change in props or state, a component will automatically re-render. Then the component updates state again, then we render again, and so on. We can fix that.

What is your intention with calling getStates? You say:


// Indicate that step 2 has an error

An error could be in response to some event. For example, the user has clicked 'submit' on a form and you have a custom validation error. That might look like this:


const ContentGroup = (props: ContentGroupProps) => {
  // Indicate that step 2 has an error
  return (
    <form
      onSubmit={(event) => {
        event.preventDefault();
        const someErrorCondition = /* snip */;
        props.getStates({
          error: someErrorCondition
        }, 2);
      }}
    >
      {/* snip */}
    </form>
  )
}

This would not cause an infinite loop in the render because a change in state does not cause ContentGroup to call the onSubmit handler again.

Sometimes you want to perform initialization and cleanup as a component mounts and unmounts:


const ContentGroup = (props: ContentGroupProps) => {
  // Indicate that step 2 has an error
  React.useEffect(() => {
    props.getStates({
      error: true
    }, 2);
    return function cleanup() {
      props.getStates({
        error: false
      }, 2);
    };
  }, []);
  return (
    <form
      onSubmit={(event) => {
        event.preventDefault();
        /* snip */
      }}
    >
      {/* snip */}
    </form>
  )
}

This does not cause an infinite loop because getStates will only be called twice: when the component mounts and right before the component unmounts.

Please treat this as pseudo-code because there are more details to leveraging hooks. Here's the documentation for useEffect, https://reactjs.org/docs/hooks-reference.html#useeffect The example code I posted won't pass many linters because it does not declare getStates as part of the hook's dependency array. In this one case, it's actually fine because we're only going to run the effect once. Further changes to getStates will be ignored. Here are more details about the dependency array of a hook: https://reactjs.org/docs/hooks-reference.html#conditionally-firing-an-effect

  •  Tags:  
  • Related