33

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.

Drew Reese
  • 165,259
  • 14
  • 153
  • 181
guillaumepotier
  • 7,369
  • 8
  • 45
  • 72
  • 1
    When routes are inside a `Switch`, only the first matching `Route` is rendered, the others are unmounted as a result. You should move Dashboard out of Switch and try.. – hazardous Aug 28 '17 at 11:47
  • @hazardous hi, you're right! Needs some tweaking though but it works! Do you want to post it as a real answer or should I answer myself with the adapted code? Best – guillaumepotier Aug 28 '17 at 12:58
  • 2
    Why are you fighting the river? If your route changes, any component no longer part of your UI unmounts. *that's how React works*, and how it should work. So, if you need data to persist independent of mounting state, just... keep it persistent outside of the component? You're in JS land, maintain your dashboard state *outside of your Dashboard component* in an object that you `require()` in (and as such the reference to which is cached), and have your Dashboard component bootstrap during `componentWillMount` based on that data. – Mike 'Pomax' Kamermans Aug 29 '17 at 00:02
  • @Mike'Pomax'Kamermans I totally agree, that's what I do in general. But regarding my Dashboard: a) I have HOC components with pagination and stuff that have their own state, that would be difficult/dirty to store on the dashboard state b) I have push events that feeds my dashboard in real time. I find it easier (just for that page) to keep the component always alive once mounted, make me do less crappy code to persist complex state and event hooks outside it – guillaumepotier Aug 29 '17 at 12:53
  • a) why? your HOCs should simply take a props, remove the keys they consume, and then pass them further in, so you're just storing a single object regardless of the number of HOCs that wrap your real component. b) realise that your dash is just a visualiser for your data. Track the data outside your component (and then then push events will keep working perfectly fine), as your component is transient by design. There is no reason for that to be crappy or complex, it's usually a straight forward refactor. – Mike 'Pomax' Kamermans Aug 29 '17 at 16:18
  • 16
    @Mike'Pomax'Kamermans I know this is a very old comment, but this advice is misleading at best - I hope newcomers don't follow it. There is a reason why components can have their own state - because in general one wants to keep the state as local as possible. Why should the whole app know about some minor implementation detail of some component? As for fighting the river, this is not *how React works*, it is *how react-router works*, and not even that (see accepted answer). So yes, keeping components mounted is a completely valid technique, and OP is right in asking how to do it. – johndodo Dec 12 '18 at 10:57
  • 1
    Not really, as a UI framework, React is all about having the components that the user needs to interact with mounted, and everything else unmounted. You can track local state in a local component, and you can even unmount and remount without losing local state, but if your _dashboard_ is for managing state about your app that comes from _outside your dashboard_ then doubling up that data makes no sense, especially since that data is demonstrably not local data. Fetch/set state locally (vanilla JS, Reflux, whatever), or remotely (e.g. with Firebase) keeps that dash a thin client. – Mike 'Pomax' Kamermans Dec 12 '18 at 16:43
  • @ancientswordrage What sort of extra explanation are you looking for or need now? Looking to keep a specific component mounted at all times? – Drew Reese May 24 '23 at 16:07
  • @DrewReese what you've supplied about react router v6 is perfect – AncientSwordRage May 24 '23 at 17:52

2 Answers2

29

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.

hazardous
  • 10,627
  • 2
  • 40
  • 52
  • 4
    This is a life savior answer. But couple of things. `render` prop didn't work for me. Only `children` prop worked. And the components that you need to keep mounted, you have to conditionally render the content, inside its `render()` function checking whether the `match` prop is undefined or not. [link](https://reacttraining.com/react-router/web/api/Route/children-func) – Sashrika Waidyarathna Jan 27 '19 at 14:21
  • 1
    example code sandbox: https://codesandbox.io/s/persist-state-react-router-1t4bl – Cherubim Oct 16 '21 at 16:44
3

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.

Drew Reese
  • 165,259
  • 14
  • 153
  • 181
  • Hello, but what if there is a Routes in the top App component and Dashboard does render a nested switch with his nested route. Dashboard will still unmount because of the top Routes from the App component – BillydogTheKid Jun 20 '23 at 11:54
  • @BillydogTheKid In the scenario you describe, is the dashboard component only being rendered with a sub-set of routes, e.g. only with specific sub-routes? If so, then yes, when the app is no longer on any of the sub-routes the dashboard component will unmount. I don't quite understand what you are asking. – Drew Reese Jun 20 '23 at 16:23
  • Yes My Dashboard that contain himself a subset of routes can only be rendered by the top Routes in the App component. There is not a single one Routes component but two one in the top App component and one nested in the Dashboard component. – BillydogTheKid Jun 22 '23 at 07:04
  • @BillydogTheKid That seems fine, though I'm also not really sure what you are asking about. I think my previous comment addressed the sub-route mounting aspect though. – Drew Reese Jun 22 '23 at 07:18
  • In RR6, this pattern fails because the router requires a to be a child of . Any way around that? – Richard Aug 22 '23 at 07:24
  • @Richard Yes, the `Route` component can only be the child of the `Routes` component or another `Route` component in the case of nested routes. What pattern fails in RRv6? Any way round *what*? Can you clarify your comment? – Drew Reese Aug 22 '23 at 07:53
  • @DrewReese in your example with RR5, you created a Dashboard route which was always mounted, and existed outside of the Switch. Is it possible to achieve something similar in RR6? Currently I've manually stored state and used `matchPath` in combination with `display: none;` in order to maintain state. – Richard Aug 22 '23 at 07:58
  • @Richard I believe the second half of my answer covers usage with RRv6. Maybe I'm still not understanding your question though. It may be of benefit (*to you*) to create a new SO post specific to your issue. You can reference this post/answer. If you create a post feel free to ping me again here with a link to it and I can take a look when available. – Drew Reese Aug 22 '23 at 08:00