Skip to content

Strategy for avoiding cascading renders #125

Closed
@ide

Description

@ide

As I understand it, when you update a Store it broadcasts an update to all Connectors. Each Connector selects the slice of State that its interested in and re-renders itself by calling this.setState. React then recursively renders the component hierarchy until it hits a nested Connector, whose componentShouldUpdate method compares the old and new props (it also compares the Connector's State, which is guaranteed not to have changed because (a) it's a slice of a Store's immutable State and (b) this nested Connector hasn't called this.setState). If the props have changed, React continues rendering the component hierarchy. Once React is done, the second, nested Connector receives an update from the Store, selects its slice of State, and re-renders itself. In the worst-case scenario, a linear chain of n Connectors could result in O(n^2) render calls.

This is a small example that demonstrates this cascading render effect. Both Connectors are subscribed to the same slice of State, and the parent passes down a prop to the child.

type StoreState = {
  user: {
    name: string;
    age: number;
  };
};

@connect(state => ({ user: state.user }))
class Parent extends Component {
  render() {
    let { user } = this.state;
    let displayName = (user.age < 18) ? getFirstName(user.name) : user.name;
    return <Child displayName={displayName} />;
  }
}

@connect(state => ({ user: state.user }))
class Child extends Component {
  render() {
    let { user } = this.state;
    return (
      <ul>
        <li>{this.props.displayName}</li>
        <li>{this.props.age}</li>
        {__DEV__ && <li>this.state.name</li>}
      </ul>
    );
  }
}

This is what I'm currently thinking:

  1. The Connectors' subscriptions to the Redux instance need to be ordered such that a parent's subscription is before its children's. I believe this is already the case so we're good here.
  2. New: when a Connector is re-rendered, it selects its slice of State from the Stores and assigns it to this._state. This means a Connector always renders with the most up-to-date State even if it hasn't been notified of a change yet. It also means that Store updates are atomic from the perspective of the subscribing views... currently a Connector's props may be derived from the new State while the Connector's state is a slice of the old State.
  3. New: when the Connector is notified of a change, it selects its slice of State from the Store and compares it against this._state. If there's no change (e.g. because we already got an up-to-date slice when the parent re-rendered this Connector) then do nothing. If there is a change then we update this._state and call this.forceUpdate.

The main thing I'm going for is the atomic Store update from the perspective of the React components. From Redux's perspective, the updates are atomic in the sense that it doesn't notify subscribers till the Stores are done transforming the State. But from the perspective of a connected component it's receiving a mix of old and new State as props, while in traditional Flux the components always read a consistent view of the Stores.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions