Multiple `setState` invocations per rendering cycle on same attribute


(Miguel Guedes) #1

I have a component that is the parent of several other components which work on data in some way. The parent component (Parent) instantiates an immutable model class that itself holds the (also immutable) state of the contained components.

Here’s how a very simplified version of the Parent component looks like:

class Parent extends React.Component {
  constructor(props)
  {
    super(props);
    this.state = {model: new Model()};
  }
  
  render()
  {
    const m = this.props.model;
    // The `onChange` handlers are previously bound to `this`.
    return (
      <div>
        <Foo model={m.foo}
             onChange={this.onFooChange}/>
        <Bar model={m.bar}
             onChange={this.onBarChange}/>
      </div>
    );
  }

  onFooChange(foo)
  { this.setState({model: this.state.model.set("foo", foo)}); }

  onBarChange(bar)
  { this.setState({model: this.state.model.set("bar", bar)}); }

The flow of data is simple and beautiful in that the components’ state is always received via their props attribute and updates are transmitted via the one onChange callback; very much flow-esque. However, there is a nagging issue.

The problem I’m having is that in some circumstances both Foo and Bar invoke their respective onChange callbacks in the same rendering cycle. In the example above, this always leads to a simple setState on the relevant attribute in the model. But since I’m using a model that contains the state of both components, the last setState invocation always replaces the state of the model and discards previous updates made in the same cycle.

So, like:

  1. render is invoked
  2. Foo changes and invokes onChange, which leads to setState on model
  3. Bar also changes and invokes onChange, which leads to setState on model, however model hasn’t yet been updated; it’s been merely queued for update above in 2). Previous state change is thus lost.

Now, I really LOVE the way I’ve got the data and updates flowing and do not want to split model into its constituent parts since Parent may actually turn out to be a child to some other component. I was then forced to devise a way to circumvent this limitation. Here’s the finished version:

class Parent extends React.Component {
  constructor(props)
  {
    super(props);
    this.model = new Model();
    this.state = {model: this.model};
  }

  componentWillUpdate(nextProps, nextState)
  { this.model = nextState.model; }
   
  render()
  {
    const m = this.props.model;
    return (
      <div>
        <Foo model={m.foo}
             onChange={this.onFooChange}/>
        <Bar model={m.bar}
             onChange={this.onBarChange}/>
      </div>
    );
  }

  update(model)
  {
    this.model = model;
    this.setState({model});
  }
  
  onFooChange(foo)
  { this.update(this.model.set("foo", foo)); }

  onBarChange(bar)
  { this.update(this.model.set("bar", bar)); }

Essentially, by relying on a class attribute (this.model) for an always up-to-date representation of the model and updating it each time setState is invoked, multiple updates per rendering cycle are guaranteed to always work. It works and strikes me as a clean-ish way of doing it. I just wish I didn’t have to duplicate the reference to model and create a formal update mechanism rather than just use setState directly.

The purpose of this post is, then, to ask the community if you know of a better way of achieving the above; if not, do you think it can be tweaked in any way?


(Develerltd) #2

Sounds like you need a way to queue up the changes, Object.assign them together, and then perform it after the render has completed?


(Alexei) #3

@miguel_guedes i had similuar issue with you and solved it by moving data to redux store.

But now i have read docs again and found that setState may accept not only object as new state.
Look here in second way function may solve your problem.


(Michel Weststrate) #4

For these kind of reasons I stopped using setState to manage local component state: https://medium.com/@mweststrate/3-reasons-why-i-stopped-using-react-setstate-ab73fc67a42e


(Aboeglin) #5

As it seems, if you use the updater function of setState you can get around that. I just came around that problem working on a Notification system where I basically had a manager component at the root of my app, and a HOC I use to provide specific components with callbacks that may show or hide notifications. Anyways, calling a show and hide in the same cycle created issues as the manager uses setState. The updater function solved the issue, you can read about it here : https://twitter.com/dan_abramov/status/824309659775467527?lang=en