50

I'd like to render some of my routes within my public layout, and some other routes within my private layout, is there a clean way to do this?

Example that obviously doesn't work, but I hope explains roughly what I'm looking for:

<Router>

  <PublicLayout>
    <Switch>
      <Route exact path="/" component={HomePage} />
      <Route exact path="/about" component={AboutPage} />
    </Switch>
  </PublicLayout>

  <PrivateLayout>
    <Switch>
      <Route exact path="/profile" component={ProfilePage} />
      <Route exact path="/dashboard" component={DashboardPage} />
    </Switch>
  </PrivateLayout>

</Router>

I'd like the layout to switch for certain routes, how do I do this with the new react router?

Nesting routes no longer works and gives me this error:

You should not use <Route component> and <Route children> in the same route; <Route children> will be ignored

Edit: Having layouts wrap entire groups of routes also means those layouts are only rendered once as long as you stay in the same private/public group of routes. This is a big deal if your layout has to fetch something from your server for example, as that would happen on every page change if you wrap each page with a layout.

Florian Bienefelt
  • 1,448
  • 5
  • 15
  • 28

12 Answers12

34

What I have done for this is create a simple component that adds an extra property to the Route component which is layout:

function RouteWithLayout({layout, component, ...rest}){
  return (
    <Route {...rest} render={(props) =>
      React.createElement( layout, props, React.createElement(component, props))
    }/>
  );
}

Then in your case your routes would look like this

<Switch>
    <RouteWithLayout layout={PublicLayout} path="/" component={HomePage}/>
    <RouteWithLayout layout={PublicLayout} path="/about" component={AboutPage}/>
    <RouteWithLayout layout={PrivateLayout} path="/profile" component={ProfilePage}/>
    <RouteWithLayout layout={PrivateLayout} path="/dashboard" component={DashboardPage}/>
</Switch>
Zaptree
  • 3,763
  • 1
  • 31
  • 26
  • Interesting... why does it not work with: ` ` – carkod Jan 13 '18 at 23:34
  • @carkod all does is make sure that only one of the routes is matched, it does not actually do what you are hoping it does with your example. In the actual scenario demonstrated it is actually not even needed. The newer version of react router just works in a very different way than previous versions. – Zaptree Jan 15 '18 at 03:50
  • Hi, with this pattern, if you want all routes to be exact you will have to pass the `exact` props to the `RouteWithLayout`: ``. Passing it to the `Route` child directly won't work. My guess is `` only consider the `exact` props of its own direct child, even if it is not a `Route` component but a wrapper. – Eric Burel Sep 26 '18 at 10:17
  • 7
    Big inconvenient here is that your layout re-renders for every route that you visit. It'd be more performant if your `Switch` was nested inside of the layout. – Florian Bienefelt Jan 18 '19 at 10:07
21

UPDATE 2023

In react router 6, I'm using this approach now:

const Pages = () => (
  <BrowserRouter>
    <Routes>
      <Route element={<GeneralLayout />}>
        <Route path="/" element={<Home />} />
        <Route path="/analysis" element={<Home />} />
        <Route path="/account/profile" element={<Profile />} />
        <Route path="/account/settings" element={<Settings />} />
      </Route>
    </Routes>
  </BrowserRouter>
);

UPDATE 2020

Well for now I'm following this approach, it's simpler that the one I posted before:

const Pages = () => {
  return (
    <ReactRouter>
      <Switch>
        <Route path="/comingsoon" component={ComingSoon} exact />
        <Route>
          <MainLayout>
            <Switch>
              <Route path="/home" exact>
                <Home />
              </Route>
              <Route path="/login" exact>
                <Login />
              </Route>
              <Route path="/useraccount" exact>
                <UserAccount />
              </Route>
              <Route path="/createaccount" exact>
                <CreateAccount />
              </Route>
              <Route path="/contact" exact>
                <Contact />
              </Route>
              <Route path="/about" exact>
                <About />
              </Route>
              <Redirect path="/" exact to="/comingsoon" />
              <Route path="*" exact component={NotFound} />
            </Switch>
          </MainLayout>
        </Route>
      </Switch>
    </ReactRouter>
  );
};

In this way, the MainLayout will take care of everything except for the coming soon page.

OLD ANSWER

If you are using Typescript and want to follow this react layout aproach then you can declare your layout like this:

import './Default.less';

import React from 'react';
import { Route } from "react-router-dom";

import { Sider } from './Components';
import { Notification } from 'Client/Components';

interface IDefaultProps {
  component: any
  path?: string;
  exact?: boolean;
}

const Default: React.SFC<IDefaultProps> = (props) => {
  const { component: Component, ...rest } = props;
  return <Route {...rest} render={matchProps => (
    <div className="defaultLayout">
      <Sider />
      <div className="defaultLayoutContent">
        <Component {...matchProps} />
      </div>
      <Notification />
    </div>
  )} />
}

export default Default;

And declare routes like this:

import React from 'react';
import { Route } from 'react-router-dom';

import { DefaultLayout } from 'Client/Layout';
import { Dashboard, Matters, Schedules, Students } from './Containers';

export const routes = <div>
  <DefaultLayout exact path="/" component={Dashboard} />
  <DefaultLayout path="/matters" component={Matters} />
  <DefaultLayout path="/schedules" component={Schedules} />
  <DefaultLayout path="/students" component={Students} />
</div>;
David Noreña
  • 3,951
  • 1
  • 28
  • 43
  • 3
    Please accept this as the correct answer. It is clean, follows current React patterns and uses Typescript. – cherry3 Jan 17 '18 at 10:42
  • 1
    Isn't it going to remount the DefaultLayout component each time the route is changed? It might have a big performance impact on the whole app and make it much harder to perform animations on the route change. – cryss Nov 19 '18 at 08:59
  • Ended up using the technique described in the React Router documentation, fit this use case well https://reacttraining.com/react-router/web/example/sidebar – ChuckJHardy May 11 '19 at 15:07
  • 1
    For v6 see https://stackoverflow.com/a/69999387/2582462 – andrzej1_1 Feb 11 '22 at 14:42
17

2019+

After looking for it, the clean and efficient way (avoiding abusive re-rendering):

    <Route exact path={["/about", "/"]}>
      <PublicLayout>
        <Route exact path="/" component={HomePage} />
        <Route path="/about" component={AboutPage} />
      </PublicLayout>
    </Route>
    <Route path={["/profile", "/dashboard"]}>
      <PrivateLayout>
        <Route path="/profile" component={ProfilePage} />
        <Route path="/dashboard" component={DashboardPage} />
      </PrivateLayout>
    </Route>

aslo, It can be refactored, see my complete answer: https://stackoverflow.com/a/57358661/3437790

David Noreña
  • 3,951
  • 1
  • 28
  • 43
Sebastien Horin
  • 10,803
  • 4
  • 52
  • 54
6

I don't think layouts belong in the route files.

Keep the route clean, ie:

<Route exact path="/" component="HomePage" />

Then, in the HomePage component, wrap the rendered content in your layout of choice:

...
render() {
  <PublicLayout>
    <h1>Home page!</h1>
  </PublicLayout>
}

This way the routes remain super clean, plus you have an easy way to display routes which should support both layouts (404 pages for instance).

cbrandolino
  • 5,873
  • 2
  • 19
  • 27
  • 4
    Imagine an app with dozens or more than a hundred routes, that means you have to repeat that wrapper layout once for every route. Just trying to keep it DRY. – Florian Bienefelt May 08 '17 at 14:27
  • 1
    Well, it's the name of one component in both cases. With the wrapper, you have some extra goodies: you can let them pass props to the children; you keep them all in one file; the next developer that opens that file knows immediately what's going on. – cbrandolino May 08 '17 at 20:20
  • 3
    This should be the accepted answer. Yes, if you have a ton of routes you'll have some duplicate code (we're only talking about 3 lines each times tbh) but it has some advantages: separate layout and routing (which are 2 different concerns), more declarative (the React way), less extra wrapper
    s. Btw, most apps will not deal with more than hundred routes and even then, layouts will not be your first problem :)
    – ClementParis016 Sep 11 '17 at 09:04
  • @ClementParis016 I totally agree with you I was thinking the same too, is not too much code to repeat, but that gives you separation of concerns ! – David Noreña Sep 12 '17 at 02:16
  • 1
    I don't think this is the "more React way" as ReactRouter training guys wrote a clear example how to do this one using a render function, as mentioned above by Zaptree https://reacttraining.com/react-router/core/api/Route/render-func – carkod Jan 13 '18 at 23:57
  • This is what we're using now and causes testing difficulties when testing the `HomePage` component. – JohnAllen Feb 13 '18 at 16:05
5

I am going to write this everywhere where this question was asked so sorry if you've seen it elsewhere. I am only doing it cause I've struggled for a while and I think it's worth spreading this solution as much as possible. Enough words, this is what I did, I think it's pretty self explanatory. Works like a charm and it's very easy to type.

const withLayout = (LayoutComp, ComponentComp) => <LayoutComp><ComponentComp /></LayoutComp>;
const withDefaultLayout = (ComponentComp) => () => withLayout(Layout, ComponentComp);
const withEmptyLayout = (ComponentComp) => () => withLayout(EmptyLayout, ComponentComp);

export const routes = <div>
    <Switch>
        <Route path="/" exact render={withDefaultLayout(Home)} />
        <Route path='/subscriptions' render={withDefaultLayout(SubscriptionsWrapped)} />

        <Route path='/callback' render={withEmptyLayout(AuthCallback)} />
        <Route path='/welcome/initial' render={withEmptyLayout(Start)} />
    </Switch>
</div>;
Peter Kottas
  • 893
  • 13
  • 27
2

I tried Florians answer but that wasn't enough for me as react will create a separate instance of your Layout for each route, hence any navigation transitions like a tab sliding will be killed.

I'm hoping for a more elegant solution, but this has got my app working again with v4.

Define one Route pointing to a component that will decide what to wrap the route in

<Router>
  <Route component={AppWrap} />
</Router>

In AppWrap do something like the following

 var isPrivateLayout;
 for (var path of listOfPrivateLayoutPaths) {
   if (this.props.location.pathname == path) {
     isPrivateLayout= true;
   }
 }
 if (isPrivateLayout) {
   return <PrivateLayout>
        (routes)
      </PrivatelyLayout>
 } else {
   return <PublicLayout>
        (routes)
      </PublicLayout>;
 }

Route Config maybe could be used for a cleaner representation of this, not sure.

1

Update: I solved it another way, but if forces you to namespace the different parts of your app with /app or /admin for example.

Each of the components UserRoutes, AdminRoutes and PublicRoutes are basically large Switch components with the specific layout at its root.

Here's how it looks:

<Router>
  <Switch>
    <Route path="/app" render={props => <UserRoutes {...props} />} />
    <Route path="/admin" render={props => <AdminRoutes {...props} />} />
    <Route path="/" render={props => <PublicRoutes {...props} />} />
  </Switch>
</Router>

Old: One solution would be to use the render prop of each Route, but it seems really cumbersome:

<Router>
  <Switch>
    <Route
      path="/"
      render={() => <PublicLayout><HomePage /></PublicLayout>}
    />
    <Route
      path="/about"
      render={() => <PublicLayout><AboutPage /></PublicLayout>}
    />
    <Route
      path="/profile"
      render={() => <PrivateLayout><ProfilePage /></PrivateLayout>}
    />
    <Route
      path="/dashboard"
      render={() => <PrivateLayout><DashboardPage /></PrivateLayout>}
    />
  </Switch>
</Router>
Florian Bienefelt
  • 1,448
  • 5
  • 15
  • 28
1

The idea is the same as that of Zaptree's but using es6 syntax and added checks so it can be used in place of react-router's Route component

Create a new component, say /src/components/Route/index.js:

import React, {Component} from 'react'
import PropTypes from 'prop-types'
import {Route as ReactRoute} from 'react-router'

class Route extends Component {
  static propTypes = {
    component: PropTypes.func.isRequired,
    layout: PropTypes.func,
    path: PropTypes.string,
    exact: PropTypes.bool
  }

  render = () => {
    const {component, layout, path, exact} = this.props
    let routeComponent = props => React.createElement(component, props)

    if (layout) {
      routeComponent = props =>
        React.createElement(layout, props, React.createElement(component, props))
    }

    return <ReactRoute path={path} exact={exact} render={routeComponent}/>
  }
}

export default Route

Use the created Route component:

import Route from 'components/Route/'
...

<Router history={createHistory()}>
  <Switch>
    <Route exact path='/' layout={PublicLayout} component={HomePage}/>
    <Route exact path='/' layout={PrivateLayout} component={ProfilePage}/>
    <Route path='/logins' component={Login}/>
  </Switch>
</Router>
Mr. 14
  • 9,228
  • 6
  • 37
  • 54
1

Use the render prop of Route component, this will not unmount and mount your layout component on every route change. More details of how this works here.

Say you have two layout components Primary.js and Secondary.js

In your App.js render method, you simply return

<BrowserRouter>
   <Switch>
     <Route path='/login' render={() => <Secondary><Login/></Secondary>} />
     <Route path='/dashboard' render={() => <Primary><Dashboard/></Primary>} />
   </Switch>
</BrowserRouter>

To refine it further, you can also define a Higher order component layout component to wrap your page component with a layout. (Not tested)

<Route to='/login' render={() => Secondary(Login)}
Anand Naik B
  • 689
  • 1
  • 7
  • 20
-1

@Qop is correct, however in the new React Router I noticed if you have your root path inside of the switch as the first route it will always match to the it and therefore never display your following routes. You should put the root path at the end.

<Switch>
    <RouteWithLayout layout={PublicLayout} path="/about" component={AboutPage}/>
    <RouteWithLayout layout={PrivateLayout} path="/profile" component={ProfilePage}/>
    <RouteWithLayout layout={PrivateLayout} path="/dashboard" component={DashboardPage}/>
    <RouteWithLayout layout={PublicLayout} path="/" component={HomePage}/>
</Switch>
Daniel
  • 91
  • 1
  • 4
-2

Same idea with @Zaptree

Layout

function PublicLayout(props) {
  return (
      <Route {...props} />
  );
}

function PrivateLayout(props) {
  return (
      <Route {...props} />
  );
}

Routes

<Switch>
    <PublicLayout exact path="/" component={HomePage} />
    <PrivateLayout path="/profile" component={ProfilePage} />
    <Route path="/callback" component={NoLayoutPage} />
</Switch>
killebytes
  • 940
  • 1
  • 10
  • 24
-5

This solution will work.

<Router>
  <Switch>
    <PublicLayout>
      <Route exact path="/" component={HomePage} />
      <Route exact path="/about" component={AboutPage} />
    </PublicLayout>
  </Switch>       

  <Switch>
    <PrivateLayout>
      <Route exact path="/profile" component={ProfilePage} />
      <Route exact path="/dashboard" component={DashboardPage} />
    </PrivateLayout>
  </Switch>    
</Router>
Paul Roub
  • 36,322
  • 27
  • 84
  • 93
Dipesh
  • 379
  • 3
  • 10
  • 2
    I proved it but it renders both layouts the same time !, can you please share the code of your layout ? – David Noreña Oct 17 '17 at 16:07
  • @DavidNoreña i am using same code and it does not render both layouts. – Dipesh Nov 23 '17 at 11:44
  • yeah it renders both layouts, but hides one very fast, also if you put console.log inside render function of both layouts you will understand what i mean .... it's a strange behaviour.... – David Noreña Dec 05 '17 at 13:33