React-router: Avoid unnecessary setState call when Route receives same props.

Created on 18 Nov 2017  Â·  5Comments  Â·  Source: ReactTraining/react-router

I just encounter a situation. I have the below App component connected to Redux. The requestQuantity changes according to current processing XHRs , which causes the App re-render.
Then the TopicList will re-render which shouldn't happen. The TopicList is also connected to Redux and doesn't depend on requestQuantity, based on the shallow comparison of Redux's connect, changing of requestQuantity has nothing to do with the TopicList's render.

class App extends Component {
  render() {
    const { requestQuantity } = this.props;
    return (
      <div>
        <Router>
          <Switch>
            <Route exact path="/" component={TopicList} />
            <Route path="/login" component={Login} />
            <Route path="/topics" component={TopicList} />
          </Switch>
        </Router>
        {requestQuantity > 0 && <Loading />}
      </div>
    );
  }
}

const mapStateToProps = (state, props) => {
  return {
    requestQuantity: getRequestQuantity(state)
  };
};

export default connect(mapStateToProps)(App);

I check the source code and find the Route's componentWillReceiveProps will always call the setState, which set a new match object. It is the new match prop passed to the TopicList causing the Redux's shallow comparison failed.

I think we can make some comparison before calling the setState. If the location, strict, exact and sensitive don't change, we can skip the setState and just passing the old state to
the components in Route. In this way , the components can render more efficiently especially when using with Redux and Mobx.

  componentWillReceiveProps(nextProps, nextContext) {
    warning(
      !(nextProps.location && !this.props.location),
      '<Route> elements should not change from uncontrolled to controlled (or vice versa). You initially used no "location" prop and then provided one on a subsequent render.'
    )

    warning(
      !(!nextProps.location && this.props.location),
      '<Route> elements should not change from controlled to uncontrolled (or vice versa). You provided a "location" prop initially but omitted it on a subsequent render.'
    )

    // do some shouldComputeMatch logic before setState
    this.setState({
      match: this.computeMatch(nextProps, nextContext.router)
    })
  }

Most helpful comment

@johnnyreilly aha, you're right, the workaround is by me.

All 5 comments

If there is a new match, we need to setState on it to ensure consistent behavior. Particularly, the location.state value makes it hard to know if we can skip an update, since that can be anything.

And if strict, exact, or sensitive are changing, that's on you. We don't control those, you do. They shouldn't be changing.

@timdorr So do you have any suggestions to achieve my requirement ? I hope the TopicList won't re-render when the location don't change(still on the same page) and only the requestQuantity changes . If the TopicList isn't used in Route, it won't re-render due to the Redux connect's shallow comparison. I think this situation isn't very rare and should have a good solution.

Thanks in advance.

Besides, the logic of shouldComputeMatch to skip the Route's setState doesn't need to be very complex, maybe just strictly comparing(===) location, strict, exact and sensitive with previous values is enough to solve this issue.

I've just bumped on this too. I'm fine with working around this, but I'd appreciate some guidance on how to best do that if possible. As @xuchaobei rightly surmised; this is not a rare scenario!

There's a workaround here: https://stackoverflow.com/questions/47375973/react-router-causes-redux-container-components-re-render-unneccessary (perhaps by original poster?)

It would be great to know if this is considered "safe":

Now my solution is creating a HOC :

// connectRoute.js
export default function connectRoute(WrappedComponent) {
  return class extends React.Component {
    shouldComponentUpdate(nextProps) {
      return nextProps.location !== this.props.location;
    }

    render() {
      return <WrappedComponent {...this.props} />;
    }
  };
}

Then use connectRoute to wrap the containers used in Route:

const PostListWrapper = connectRoute(PostList);
const LoginWrapper = connectRoute(Login);

class App extends Component {
  render() {
    const { requestQuantity } = this.props;
    return (
      <div>
        <Router>
          <Switch>
            <Route exact path="/" component={PostListWrapper} />
            <Route path="/login" component={LoginWrapper} />
            <Route path="/topics" component={PostListWrapper} />
          </Switch>
        </Router>
        {requestQuantity > 0 && <Loading />}
      </div>
    );
  }
}

@johnnyreilly aha, you're right, the workaround is by me.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

ackvf picture ackvf  Â·  3Comments

maier-stefan picture maier-stefan  Â·  3Comments

jzimmek picture jzimmek  Â·  3Comments

Radivarig picture Radivarig  Â·  3Comments

andrewpillar picture andrewpillar  Â·  3Comments