React-router: never unmount a component on a route once mounted, even if route change

28.8k views Asked by At

I have a React application that declares some routes:

<Switch>
  <Route exact path={'/'} render={this.renderRootRoute} />
  <Route exact path={'/lostpassword'} component={LostPassword} />
  <AuthenticatedRoute exact path={'/profile'} component={Profile} session={session} redirect={'/'} />
  <AuthenticatedRoute path={'/dashboard'} component={Dashboard} session={session} redirect={'/'} />
  <AuthenticatedRoute path={'/meeting/:meetingId'} component={MeetingContainer} session={session} redirect={'/'} />
  <Route component={NotFound} />
</Switch>

(AuthenticatedRoute is a dumb component that checks the session, and either call <Route component={component} /> or <Redirect to={to} />, but at last, component method is invoked)

Where basically each component is mounted/unmounted on route change. I'd like to keep that aspect except for the Dashboard route which does a lot of things, and that I would like to be unmounted once not on dashboard (let's say you arrive on a meeting page, you do not need to mount your dashboard yet) but once you loaded once your Dashboard, when you go on your profile page, a meeting or whatever, when you go back on your Dashboard the component does not have to mount again.

I read on React-router doc that render or children might be the solution, instead of component, but could we mix routes with children and other with component? I tried many things and never achieved what I wanted, even with render or children, my Dashboard component is still mounting/unmounting.

2

There are 2 answers

2
hazardous On BEST ANSWER

The Switch component only ever renders a single route, the earliest match wins. As the Dashboard component is inside it, it gets unmounted whenever another route gets matched. Move this Route outside and it will work as intended with render or children.

8
Drew Reese On

This is how the Switch component works, it simply "renders the first child <Route> or <Redirect> that matches the location." The Switch exclusively renders routes/redirects, e.g. only 1 can be matched and rendered. This is different than routes/redirects rendered just within a router component that inclusively renders anything that matches.

If you want to unconditionally render the Dashboard then move its route outside the switch to exclude it from the Switch.

Use the children function prop to unconditionally render the route and then decide what to actually render. The Dashboard component can be configured to remain mounted and conditionally render its own content.

Example:

const Dashboard = ({ isMatch }) => {
  ... dashboard business logic ...

  return isMatch
    ? ( ... dashboard UI ... )
    : null; // hidden
};
<AuthenticatedRoute
  path="/dashboard"
  session={session}
  redirect="/"
  children={({ match }) => <Dashboard isMatch={match} />}
/>
<Switch>
  <Route exact path="/" render={this.renderRootRoute} />
  <Route exact path="/lostpassword" component={LostPassword} />
  <AuthenticatedRoute exact path="/profile" component={Profile} session={session} redirect="/" />
  <AuthenticatedRoute path="/meeting/:meetingId" component={MeetingContainer} session={session} redirect="/" />
  <Route component={NotFound} />
</Switch>

You may need to tweak the AuthenticatedRoute component to correctly consume route component props. This allows passing any of the valid Route component props to AuthenticatedRoute to be passed on to Route, e.g. path, component, render, children, exact, etc....

Example:

const AuthenticatedRoute = ({ redirect, session, ...props }) => {
  return /* auth condition using session */
    ? <Route {...props} />
    : <Redirect to={redirect} />;
};

Similarly, in react-router-dom@6 where the Routes component replaced the Switch component, it selects the single best match and renders it. The main difference between Routes and Switch though is that RRDv6 uses a route ranking system to select the best match instead of relying on the code maintainer to correctly order the routes in the Switch by inverse order of path specificity. The same rules apply to when routed components are rendered though, e.g. when the route they are rendered on is the currently matched route, the routed component is mounted, otherwise it is unmounted.

Again, if you want to unconditionally render the Dashboard component it should be rendered outside the Routes on its own.

<Dashboard /> // *
<Routes>
  <Route path="/" element={this.renderRootRoute} />
  <Route path="/lostpassword" element={<LostPassword />} />
  <Route element={<AuthenticatedRoute session={session} redirect="/" />}>
    <Route path="/profile" element={<Profile />} />
    <Route path="/meeting/:meetingId" element={<MeetingContainer />} />
  </Route>
  <Route path="*" element={<NotFound />} />
</Routes>

* Note: You can update the Dashboard component to use any internal condition to conditionally render any specific content, if necessary.

If you want the Dashboard to render only with specific routes then create a layout route that renders it and an Outlet for nested routes.

Example:

import { Outlet } from 'react-router-dom';

export const DashboardLayout = () => (
  <>
    <Dashboard />
    <Outlet />
  </>
);

For example, if you wanted Dashboard to only render along with the authenticated routes:

<Routes>
  <Route path="/" element={this.renderRootRoute} />
  <Route path="/lostpassword" element={<LostPassword />} />
  <Route element={<AuthenticatedRoute session={session} redirect="/" />}>
    <Route element={<DashboardLayout />}>
      <Route path="/profile" element={<Profile />} />
      <Route path="/meeting/:meetingId" element={<MeetingContainer />} />
    </Route>
  </Route>
  <Route path="*" element={<NotFound />} />
</Routes>

The Dashboard will remain mounted and rendered so long as one of its nested routes is being matched and rendered.