React Router causes Redux container components re-render unneccessary

Viewed 1940

Here is my major code, App component is connected to Redux's store:

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

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

export default connect(mapStateToProps)(App);

PostList component is also connected to Redux's store:

class PostList extends Component {
  componentDidMount() {
    this.props.fetchAllPosts();
  }

  render() {
    const { posts} = this.props;
    return (
      // ...
    );
  }

  //...
}

const mapStateToProps = (state, props) => {
  return {
    posts: getPostList(state),
  };
};

const mapDispatchToProps = dispatch => {
  return {
    ...bindActionCreators(postActions, dispatch),
  };
};

export default connect(mapStateToProps, mapDispatchToProps)(PostList);

When this.props.fetchAllPosts() is called, the requestQuantity in the global state will change from 0 to 1 (request starts) then to 0 (request ends). So the App will re-render twice. However, every re-rendering of App also causes PostList to re-render, which is what I don't expect, since PostList only depends on posts in the global state and posts doesn't change in these twice re-rendering.

I check React Router's source code and find the Route's componentWillReceiveProps will always call the setState, which set a new match object:

  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.'
    )

    //the official always set a new match object ignoring whether the nextProps change or not
    this.setState({
      match: this.computeMatch(nextProps, nextContext.router)
    })
  }

It is the new match prop passed to the PostList causing the Redux's shallow comparison fails and re-rendering occurs. I hope React Router's team can do some easy logic before setState, such as using (===) comparing every prop in nextProps and this.props, if no change occurs, skip setState. Unfortunately,they think it is not a big deal and closed my issue.

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>
    );
  }
}

Besides, when React Router is used with Mobx, this issue is also easy to meet.

Hope someone could offer better solutions. A long question. Thanks for your patience.

0 Answers
Related