Repeated rendering of components caused by react router

Time:2021-4-30

4 react routersRouteAs a common react component, it can be used in any componentRouteInstead of having to define all of them in one place, as in previous versionsRoute. Therefore, in projects using react router 4, there are oftenRouteAnd other components in the same component. For example, the following code:

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

Page loading effect componentLoadingandRouteAt the same level, so,HomeLoginAnd other page components share the outer loading component. When used with Redux, isrequesting is stored in the store of redux,AppAs container components in Redux, isrequesting is obtained from the store.HomeLoginAs a container component, the root component of the page will get the required state from the store to render the component. The code evolved as follows:

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

const mapStateToProps = (state, props) => {
  return {
    isRequesting: getRequestingState(state)
  };
};

export default connect(mapStateToProps)(App);
class Home extends Component {
  componentDidMount() {
    this.props.fetchHomeDataFromServer();
  }
  
  render() {
    return (
      <div>
       {homeData}
      </div>
    );
  }
}

const mapStateToProps = (state, props) => {
  return {
    homeData: getHomeData(state)
  };
};

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

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

HomeAfter the component is mounted, callthis.props.fetchHomeDataFromServer()This asynchronous action gets the data needed for the page from the server.fetchHomeDataFromServerThe general structure is as follows:

const fetchHomeDataFromServer = () => {
  return (dispatch, getState) => {  
    dispatch(REQUEST_BEGIN);
    return fetchHomeData().then(data => {
      dispatch(REQUEST_END);   
      dispatch(setHomeData(data));
    });    
}

In this way, in thedispatch setHomeData(data)Before, willdispatchThe other two actions change isrequesting to controlAppinLoadingDisplay and hide of. Normally, a change in isrequesting should only lead toAppComponent rerender without affectingHomeComponents. Because after Redux connectHomeComponent. In the update phase, shallow comparison will be used to determine whether the received props have changed. If not, the component will not be rerendered.HomeThe component does not rely on isrequesting, and the render method should not be triggered.

But the actual result is, every timeAppThe re render of, are accompanied byHomeRerender of. The optimization done by Redux is wasted!

What is the reason? Finally, I’m at react routerRouteThe culprit is found in the source code of

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

    //Note here that computematch returns a new object each time. In this way, every time route is updated, setstate will reset a new match object
    this.setState({
      match: this.computeMatch(nextProps, nextContext.router)
    })
  }

  render() {
    const { match } = this.state
    const { children, component, render } = this.props
    const { history, route, staticContext } = this.context.router
    const location = this.props.location || route.location
    //Notice here that this is the property passed to the component in the route
    const props = { match, location, history, staticContext }

    if (component)
      return match ? React.createElement(component, props) : null

    if (render)
      return match ? render(props) : null

    if (typeof children === 'function')
      return children(props)

    if (children && !isEmptyChildren(children))
      return React.Children.only(children)

    return null
  }

RouteOfcomponentWillReceivePropsThesetStateSet match, which is controlled bycomputeMatchBy calculation,computeMatchA new object is returned each time. So, every timeRouteUpdate (componentwillreceiveprops is called), a new match will be created, and this match will be passed as props to byRouteIn this case, theHome)。 therefore,HomeComponents always receive a new request during the update phasematchAttribute, which causes the shallow comparison failure of Redux, and then triggers the re rendering of components. In fact, in the case above,RoutePass on toHomeThe other attributes of location, history and staticcontext have not changed. Although match is a new object, the content of the object has not changed (it is always on the same page, the URL has not changed, and the calculation result of match has not changed).

If you think this problem is only encountered with Redux, it’s a big mistake. Take two scenarios without Redux:

  1. AppThe structure is basically unchanged, except that isrequesting is no longer obtained through Redux, but is maintained as the state of the component itself.HomeInherited fromReact.PureComponentHomeadoptAppTo change isrequesting,AppRe render, for the same reason,HomeIt will also be renewed.React.PureComponentAnd it’s wasted.
  2. In combination with mobx,AppandHomeComponent passed@observermodification,AppListen to isrequesting change rerender, for the same reason,HomeComponents are also rerendered.

OneRouteThe optimization of state management library is greatly reduced! It hurts!

I have mentioned this to react router on GitHubissueWe hope to be able tocomponentWillReceivePropsMake some simple judgments first, and then decide whether to reconsidersetState. However, it is disappointing that this issue was quickly closed by a collaborator.

Well, it’s better to ask others than yourself, and find a solution by yourself.

There are several ideas

  1. sinceLoadingPut in andRouteIf this problem occurs in components at the same level, thenLoadingPut it in a lower level component,HomeLoginNo matter how many timesLoadingComponents. But this method is not a cure,HomeThere may still be other definitions in the componentRouteHomeThe update of dependency state will also lead to these problemsRouteRe rendering of internal components. In other words, as long as theRouteThis problem cannot be solved. But in react router 4RouteIt is impossible to avoid using it completely in container componentsRouteYes.
  2. Rewriting of container componentsshouldComponentUpdateMethod, the method is feasible, but each component rewrites again, heart tired.
  3. Next, the idea of 2 is to create a high-order component and rewrite it in the high-order componentshouldComponentUpdate, ifRouteIf the location property passed has not changed (indicating that it is on the same page), false is returned. Then use this high-level component to wrap each one in theRouteComponents used in.

    Create a new high-level componentconnectRoute:

    import React from "react";
    
    export default function connectRoute(WrappedComponent) {
      return class extends React.Component {
        shouldComponentUpdate(nextProps) {
          return nextProps.location !== this.props.location;
        }
    
        render() {
          return <WrappedComponent {...this.props} />;
        }
      };
    }
    

    useconnectRoutepackageHomeLogin

    const HomeWrapper = connectRoute(Home);
    const LoginWrapper = connectRoute(Login);
    
    class App extends Component {
      render() {
        const { isRequesting } = this.props;
        return (
          <div>
            <Router>
              <Switch>
                <Route exact path="/" component={HomeWrapper} />
                <Route path="/login" component={LoginWrapper} />
                <Route path="/home" component={HomeWrapper} />
              </Switch>
            </Router>
            {isRequesting  && <Loading />}
          </div>
        );
      }
    }

This will solve the problem once and for all.

Let’s think about another scenario, ifAppThe state of use also affectsRouteFor exampleisRequestingWhen true, the thirdRouteThe path will also change, assuming that<Route path="/home/fetching" component={HomeWrapper} />, andHomeIt will be used internallyRouteThe path passed (actually through thematch.pathAt this time, we need toHomeComponent rerender. But because of the complexity of higher-order componentsshouldComponentUpdateWe just make a judgment according to the location, and the location has not changed at this timeHomeIt does not re render. It’s a very special scenario, but you want to tell you through this scenario that higher-level componentsshouldComponentUpdateThe decision-making should be made according to the actual business scenario. In most scenarios, the high-level components above are enough.

RouteThe use of posture is not simple, and line and cherish it!


Welcome to my official account: the front end of veteran cadres, and receive 21 books from the front end.

Repeated rendering of components caused by react router

Recommended Today

JS generate guid method

JS generate guid method https://blog.csdn.net/Alive_tree/article/details/87942348 Globally unique identification(GUID) is an algorithm generatedBinaryCount Reg128 bitsNumber ofidentifier , GUID is mainly used in networks or systems with multiple nodes and computers. Ideally, any computational geometry computer cluster will not generate two identical guids, and the total number of guids is2^128In theory, it is difficult to make two […]