Florian Bienefelt
Florian Bienefelt

Reputation: 1578

React Router v4 with multiple layouts

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.

Upvotes: 50

Views: 33198

Answers (12)

David Nore&#241;a
David Nore&#241;a

Reputation: 4230

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>;

Upvotes: 21

Sebastien Horin
Sebastien Horin

Reputation: 11067

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

Upvotes: 17

Anand Naik B
Anand Naik B

Reputation: 769

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)}

Upvotes: 1

Daniel
Daniel

Reputation: 101

@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>

Upvotes: -1

Dipesh
Dipesh

Reputation: 379

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>

Upvotes: -5

killebytes
killebytes

Reputation: 980

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>

Upvotes: -2

Peter Kottas
Peter Kottas

Reputation: 943

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>;

Upvotes: 5

Mr. 14
Mr. 14

Reputation: 9528

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>

Upvotes: 1

cbrandolino
cbrandolino

Reputation: 5883

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

Upvotes: 6

Zaptree
Zaptree

Reputation: 3803

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>

Upvotes: 34

Florian Bienefelt
Florian Bienefelt

Reputation: 1578

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>

Upvotes: 1

Dan Banfield
Dan Banfield

Reputation: 61

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.

Upvotes: 2

Related Questions