React-Router - Route re-rendering component on route change

James Morrison picture James Morrison · Jan 9, 2019 · Viewed 11.3k times · Source

Please read this properly before marking as duplicate, I assure you I've read and tried everything everyone suggests about this issue on stackoverflow and github.

I have a route within my app rendered as below;

<div>
        <Header compact={this.state.compact} impersonateUser={this.impersonateUser} users={users} organisations={this.props.organisations} user={user} logOut={this.logout} />
        <div className="container">
            {user && <Route path="/" component={() => <Routes userRole={user.Role} />} />}
        </div>
    {this.props.alerts.map((alert) =>
            <AlertContainer key={alert.Id} error={alert.Error} messageTitle={alert.Error ? alert.Message : "Alert"} messageBody={alert.Error ? undefined : alert.Message} />)
        }
    </div>

The route rendering Routes renders a component that switches on the user role and lazy loads the correct routes component based on that role, that routes component renders a switch for the main pages. Simplified this looks like the below.

import * as React from 'react';
import LoadingPage from '../../components/sharedPages/loadingPage/LoadingPage';
import * as Loadable from 'react-loadable';

export interface RoutesProps {
    userRole: string;
}

const Routes = ({ userRole }) => {

var RoleRoutesComponent: any = null;
switch (userRole) {
    case "Admin":
        RoleRoutesComponent = Loadable({
            loader: () => import('./systemAdminRoutes/SystemAdminRoutes'),
            loading: () => <LoadingPage />
        });
        break;
    default:
        break;
}

return (
    <div>
        <RoleRoutesComponent/> 
    </div>
);

}

export default Routes;

And then the routes component

const SystemAdminRoutes = () => {

var key = "/";

return (
    <Switch>
        <Route key={key} exact path="/" component={HomePage} />
        <Route key={key} exact path="/home" component={HomePage} />
        <Route key={key} path="/second" component={SecondPage} />
        <Route key={key} path="/third" component={ThirdPage} />
        ...
        <Route key={key} component={NotFoundPage} />
    </Switch>
);
}

export default SystemAdminRoutes;

So the issue is whenever the user navigates from "/" to "/second" etc... app re-renders Routes, meaning the role switch logic is rerun, the user-specific routes are reloaded and re-rendered and state on pages is lost.

Things I've tried;

  • I've tried this with both react-loadable and React.lazy() and it has the same issue.
  • I've tried making the routes components classes
  • Giving all Routes down the tree the same key
  • Rendering all components down to the switch with path "/" but still the same problem.
  • Changing Route's component prop to render.
  • Changing the main app render method to component={Routes} and getting props via redux
  • There must be something wrong with the way I'm rendering the main routes component in the app component but I'm stumped, can anyone shed some light? Also note this has nothing to do with react-router's switch.

    EDIT: I've modified one of my old test project to demonstrate this bug, you can clone the repo from https://github.com/Trackerchum/route-bug-demo - once the repo's cloned just run an npm install in root dir and npm start. I've got it logging to console when the Routes and SystemAdminRoutes are re-rendered/remounted

    EDIT: I've opened an issue about this on GitHub, possible bug

    Route re-rendering component on every path change, despite path of "/"

    Answer

    James Morrison picture James Morrison · Jan 10, 2019

    Found the reason this is happening straight from a developer (credit Tim Dorr). The route is re-rendering the component every time because it is an anonymous function. This happens twice down the tree, both in App and Routes (within Loadable function), below respectively.

    <Route path="/" component={() => <Routes userRole={user.Role} />} />
    

    needs to be

    <Routes userRole={user.Role} />
    

    and

    loader: () => import('./systemAdminRoutes/SystemAdminRoutes')
    

    Basically my whole approach needs to be rethought

    EDIT: I eventually fixed this by using the render method on route:

    <Route path="/" render={() => <Routes userRole={user.Role} />} />