React: [Updates] Bailing out of a transactional setState call

Created on 1 Oct 2015  路  10Comments  路  Source: facebook/react

For practical reasons about performance and energy conservation (in this sense, all React components are non-idempotent and have side effects), I occasionally will check my state before setting it:

if (this.state.something) {
  this.setState({ ... });
}

I would like to be able to do this with the transactional version of setState as well:

this.setState(state => {
  if (state.something) {
    return { ... };
  }
  // if null or undefined (or false?) is returned, don't re-render!
});

A toy example of this is a piece of component state that gets updated during each scroll event. Due to event batching (especially with RN), multiple scroll events can fly in during the same batch.

<ScrollView onScroll={() => {
  this.setState(state => {
    if (state.numberOfScrollEvents < 10) {
      return { numberOfScrollEvents: state.numberOfScrollEvents + 1 };
    }
  });
} />

Most helpful comment

We allow returning null from updaters to prevent an update in 16.

All 10 comments

cc @sebmarkbage

We could potentially bail out if you return the same state object. It's probably what you would do if you mutate it but that's bad practice anyway. Returning undefined is not nice because it lets accidental implicit returns continue. false makes it a union type. Returning null is also not nice for types. You have to declare it as nullable.

I'm not sure this is a pattern we should encourage to check for manually though.

shouldComponentUpdate exists to let you avoid reconciling based on previous state and next state. So you can always bail out the reconciliation.

The only thing you're bailing out from is calculating the new state value. So, what are you doing that is expensive to calculate?

Btw, IMO, RN (and other event systems) shouldn't fire multiple events in a batch but fire a list of events as a single callback. Like iOS does for touches.

With shouldComponentUpdate I don't have as much context to (easily) determine whether I can bail out. With shouldComponentUpdate I don't know why the state changed, or whether there was a state change at all (could have been a props change). It's much easier for me to write that code near the setState call in a cohesive way.

RN (and other event systems) shouldn't fire multiple events in a batch but fire a list of events as a single callback.

I'm stretching here, but the same scenario arises if I am handling two different types of events that aren't grouped in a single callback, like a network response and a touch event. RN may batch these in the same event loop.

btw this.setState() with no argument also triggers a rerender.

@spicyj Yeah, I noticed that. What I kind of want is a way to schedule a callback that is run when the pending-state queue is processed (at the same time the transactional setState callbacks are invoked):

this.requestPendingState(state => {
  this.setState({...});  // instead of returning a value, you call setState and it is immediately applied
}, /* optional callback w/same semantics as setState's 2nd arg */);

I think it's a fun idea but there might be reasons against it and that ship has kind of sailed :)

requestPendingState was discussed as a possibility but it's tricky because you may be encouraged to do other side-effects.

All of this leads to a very imperative way of coding and any optimizations you add can have fragile assumptions about what could affect what. shouldComponentUpdate is a funnel that ensures that you don't do those assumptions.

Could you add some more context? It seems like if you need information about why something happened, then the system might be relying too much on imperative programming already. I.e. is it leaky?

And if you do need to rely on an imperative style, you're probably better off putting it on an instance variable _myImperativeStateIKnowWhatImDoing instead of using setState.

Would the same optimization be possible/relevant in these pure patterns? https://github.com/reactjs/react-future/tree/master/07%20-%20Returning%20State

I was writing a pull-to-refresh component that looks at each scroll event. If the scroll position is negative (the user is pulling down), then I want to re-render the refresh indicator by updating my component's state. This state update happens to depend on this.state, so I used the transactional version of setState. When the scroll position is positive, however, I know that I don't need to do any state updates, so my code roughly looks like this:

_onScroll(scrollPosition) {
  if (scrollPosition > 0) {
    return;
  }

  this.setState(state => ({
    refreshing: state.touching && someOtherStuff,
    progress: calculateProgressFromScrollPosition(scrollPosition),
  });
}

I would be out of luck if I needed to look a piece of state (ex: whether the user is touching the scroll view) in addition to scrollPosition to see if I could return early.

I'm not sure how I would bail out from shouldComponentUpdate since there are other props and state fields that are mutable values. I think would need to know that the state change is just { refreshing: true/false } and possibly some extra context info like the scroll position.

Philosophically I also believe the shouldComponentUpdate funnel may not always be the best place for these optimizations. When the heuristic is relevant regardless of how the component was updated (any combination of props, state, and context) then the funnel makes a lot of sense to me. But thinking about this principle of high cohesion, my intuition is that some of these optimizations belong closer to the setState call.


The reason to avoid re-rendering with the the pull-to-refresh component is that it usually wraps a ListView, which iterates over all of its row items to determine if anything has changed. In my application I have a very efficient rowHasChanged function that does identity equality checks between old and new row items since I use immutable data structures. But in the general case, other people using my component may have a more expensive rowHasChanged check or they may always return true, in which case React would proceed to reconcile all of their row components. All this could be avoided by not updating my pull-to-refresh component.


Would the same optimization be possible/relevant in these pure patterns? https://github.com/reactjs/react-future/tree/master/07%20-%20Returning%20State

Yeah, I think so. Looking at proposals 1/2/3, I think your earlier idea about bailing out if the same state object is returned is really appealing. That's especially true when you want to handle an event without always changing state. With proposal 4 I guess you would just not call update.

Philosophically, I'm worried about multiple setStates. My ideal would something like:

https://github.com/reactjs/react-future/blob/master/09%20-%20Reduce%20State/01%20-%20Declarative%20Component%20Module.js#L32

That way you can follow any possible path that could've effected the state.

I've seen way to many patterns that effectively lead to:

handleClick() {
  this.setState(..., () => doSomething().then(() => this.setState(...)).then(() => this.setState(...));
}

iHopeThisDoesntFireInTheMiddleOfSomething() {
  this.setState(...);
}

Yet, covered by abstractions. This hides state in closures and is constantly relying on an implicit "action" being traced through the state changes to take shortcuts.

I'm not sure how I would bail out from shouldComponentUpdate since there are other props and state fields that are mutable values.

React doesn't work properly with mutable values if you rely on a props mutation triggering a deep state update for consistency. I.e. the component that mutated a value needs to call this.forceUpdate() or this.setState(...) so that the parent updates and the flushes down. Additionally, React bails out if you try to reuse a ReactElement with mutated props. This means that for mutation to work, you will always get a new props object if your parent rerendered.

Therefore this solution should work for you:

shouldComponentUpdate(nextProps, nextState) {
  var stateChanged = nextState.refreshing !== this.state.refreshing ||
                     nextState.progress !== this.state.progress;
  var propsChanged = this.props !== nextProps;
  return stateChanged || propsChanged;
}

If the only change was due to your setState then propsChanged will be false. If you're at a positive scroll position you'll keep setting the same state so stateChanged will also be false.

If you rerender with mutated props, propsChanged will be true.

That way your state transitions can be described completely in terms of a reducer and any optimizations/shortcuts are completely isolated to shouldComponentUpdate.

Re: the mutable values -- the code doesn't rely on deep updates. I meant something like this:

constructor() {
  this.state = { model: null, /* other fields */ };
}
onDataStoreUpdate(model) {
  // model could be a mutable object that the data store mutates when
  // the model's data changes. The store still calls `onDataStoreUpdate`
  // and we call `setState`, so there's no relying on `forceUpdate`
  this.setState({ model });
}
render() {
  // Is the re-rendering of Subcomponent what you consider a "deep update for
  // state consistency"?
  return <Subcomponent model={model} />;
}
shouldComponentUpdate(nextProps, nextState) {
  // Cannot reliably tell if nextState.model changed since it's mutable
}

you will always get a new props object if your parent rerendered.

This is pretty helpful to know I can rely on that. In my actual use case this helps a lot. I do still want to be able to use this.setState(this.state) to signal that there should be no update (what you're doing here: https://github.com/reactjs/react-future/blob/master/09%20-%20Reduce%20State/01%20-%20Declarative%20Component%20Module.js#L41). It is pretty convenient.

We allow returning null from updaters to prevent an update in 16.

Was this page helpful?
0 / 5 - 0 ratings