Using React-Router with a layout page or multiple components per page

ReactjsReact Router

Reactjs Problem Overview


I am adding react router to an existing project.

At present a model is passed in to a root component which contains a navigation component for the sub navigation and a main component.

The examples of react router I've found only have one child component, what is the best way to have multiple child components change without repeating the layout code in both?

Reactjs Solutions


Solution 1 - Reactjs

If I understood you correctly, to achieve that you would define multiple components in your Route. You can use it like:

// think of it outside the context of the router, if you had pluggable
// portions of your `render`, you might do it like this
<App children={{main: <Users/>, sidebar: <UsersSidebar/>}}/>

// So with the router it looks like this:
const routes = (
  <Route component={App}>
    <Route path="groups" components={{main: Groups, sidebar: GroupsSidebar}}/>
    <Route path="users" components={{main: Users, sidebar: UsersSidebar}}>
      <Route path="users/:userId" component={Profile}/>
    </Route>
  </Route>
)

class App extends React.Component {
  render () {
    const { main, sidebar } = this.props;
    return (
      <div>
        <div className="Main">
          {main}
        </div>
        <div className="Sidebar">
          {sidebar}
        </div>
      </div>
    )
  }
}

class Users extends React.Component {
  render () {
    return (
      <div>
        {/* if at "/users/123" `children` will be <Profile> */}
        {/* UsersSidebar will also get <Profile> as this.props.children,
            so its a little weird, but you can decide which one wants
            to continue with the nesting */}
        {this.props.children}
      </div>
    )
  }
}

Also check out the sidebar example app, should help you more.

Edit: As per @Luiz's comment: > In the latest version of router (v3) the components are in the root of the props object

So:

const { main, sidebar } = this.props.children;

becomes:

const { main, sidebar } = this.props;

EDIT: In the react-router v4 this can be accomplished like (as per the example provided in the new docs):

import React from 'react'
import {
  BrowserRouter as Router,
  Route,
  Link
} from 'react-router-dom'

// Each logical "route" has two components, one for
// the sidebar and one for the main area. We want to
// render both of them in different places when the
// path matches the current URL.
const routes = [  { path: '/',    exact: true,    sidebar: () => <div>home!</div>,    main: () => <h2>Home</h2>  },  { path: '/bubblegum',    sidebar: () => <div>bubblegum!</div>,    main: () => <h2>Bubblegum</h2>  },  { path: '/shoelaces',    sidebar: () => <div>shoelaces!</div>,    main: () => <h2>Shoelaces</h2>  }]

const SidebarExample = () => (
  <Router>
    <div style={{ display: 'flex' }}>
      <div style={{
        padding: '10px',
        width: '40%',
        background: '#f0f0f0'
      }}>
        <ul style={{ listStyleType: 'none', padding: 0 }}>
          <li><Link to="/">Home</Link></li>
          <li><Link to="/bubblegum">Bubblegum</Link></li>
          <li><Link to="/shoelaces">Shoelaces</Link></li>
        </ul>

        {routes.map((route, index) => (
          // You can render a <Route> in as many places
          // as you want in your app. It will render along
          // with any other <Route>s that also match the URL.
          // So, a sidebar or breadcrumbs or anything else
          // that requires you to render multiple things
          // in multiple places at the same URL is nothing
          // more than multiple <Route>s.
          <Route
            key={index}
            path={route.path}
            exact={route.exact}
            component={route.sidebar}
          />
        ))}
      </div>

      <div style={{ flex: 1, padding: '10px' }}>
        {routes.map((route, index) => (
          // Render more <Route>s with the same paths as
          // above, but different components this time.
          <Route
            key={index}
            path={route.path}
            exact={route.exact}
            component={route.main}
          />
        ))}
      </div>
    </div>
  </Router>
)

export default SidebarExample

Make sure you check out the new React Router v4 docs here: https://reacttraining.com/react-router/

Solution 2 - Reactjs

2019 +

The simple and clean way to do it and avoid abusive re-rendering is (tested on react router v5, need to be confirmed on react router v4):

       <Switch>
         <Route exact path={["/route1/:id/:token", "/"]}>
          <Layout1>
            <Route path="/route1/:id/:token" component={SetPassword} />
            <Route exact path="/" component={SignIn} />
          </Layout1>
        </Route>
        <Route path={["/route2"]}>
          <Layout2>
            <Route path="/route2" component={Home} />
          </Layout2>
        </Route>
      </Switch>

which can be refactored to:

const routes = [
  {
    layout:Layout1,
    subRoutes:[
      {
        path:"/route1/:id/:token",
        component:SetPassword
      },
      {
        exact:true,
        path:"/",
        component:SignIn
      },
    ]
  },
  {
    layout:Layout2,
    subRoutes:[
      {
        path:"/route2",
        component:Home
      },
    ]
  }
];

with:

      <Switch>
        {routes.map((route,i)=>
          <Route key={i} exact={route.subRoutes.some(r=>r.exact)} path={route.subRoutes.map(r=>r.path)}>
            <route.layout>
              {route.subRoutes.map((subRoute,i)=>
                <Route key={i} {...subRoute} />
              )}
            </route.layout>
          </Route>
        )}
      </Switch>

Solution 3 - Reactjs

To add upon Sebastien's answer, this seems to work for me, including a not found route and dynamic subroutes. The examples below make my LayoutAuthenticated and LayoutAnonymous just render once, not on every route change within routes that use that same layout. Also added the PageSettings example to show nested routes within this architecture. Hope this might help others!

(examples include TypeScript)

const publicRoutes = [
  {
    key: "login",
    path: "/login",
    component: PageLogin,
    exact: true
  },
  {
    key: "signup",
    path: "/signup",
    component: PageSignup,
    exact: true
  },
  {
    key: "forgot-password",
    path: "/forgot-password",
    component: PageForgotPassword,
    exact: true
  }
];

const privateRoutes = [
  {
    key: "home",
    path: "/",
    component: PageHome,
    exact: true
  },
  {
    key: "settings",
    path: "/settings",
    component: PageSettings, // sub routing is handled in that component
    exact: false // important, PageSettings is just a new Router switch container
  }
];
// Routes.tsx

<Router>
  <Switch>
    <Route exact path={["/", "/settings", "/settings/*"]}>
      <LayoutAuthenticated>
        <Switch>
          {privateRoutes.map(privateRouteProps => (
            <PrivateRoute {...privateRouteProps} />
          ))}
        </Switch>
      </LayoutAuthenticated>
    </Route>

    <Route exact path={["/login", "/signup", "/forgot-password"]}>
      <LayoutAnonymous>
        <Switch>
          {publicRoutes.map(publicRouteProps => (
            <PublicRoute {...publicRouteProps} />
          ))}
        </Switch>
      </LayoutAnonymous>
    </Route>

    <Route path="*">
      <LayoutAnonymous>
        <Switch>
          <Route component={PageNotFound} />
        </Switch>
      </LayoutAnonymous>
    </Route>
  </Switch>
</Router>
// LayoutAnonymous.tsx

import React from 'react';

export const LayoutAnonymous: React.FC<{}> = props => {
  return (
    <div>
      {props.children}
    </div>
  )
}

// LayoutAuthenticated.tsx

import React from 'react';
import { MainNavBar } from '../components/MainNavBar';
import { MainContent } from '../components/MainContent';

export const LayoutAuthenticated: React.FC<{}> = props => {
  return (
    <>
      <MainNavBar />
      <MainContent>
        {props.children}
      </MainContent>
    </>
  )
}


// PrivateRoute.tsx

import React from "react";
import {
  Route,
  Redirect,
  RouteProps
} from "react-router-dom";
import { useSelector } from "react-redux";

interface Props extends RouteProps {}

export const PrivateRoute: React.FC<Props> = props => {
  const isAuthenticated: boolean = useSelector<any, any>((stores) => stores.auth.isAuthenticated);

  const { component: Component, ...restProps } = props;

  if (!Component) return null;

  return (
    <Route
      {...restProps}
      render={routeRenderProps =>
        isAuthenticated ? (
          <Component {...routeRenderProps} />
        ) : (
          <Redirect
            to={{
              pathname: "/login",
              state: { from: routeRenderProps.location }
            }}
          />
        )
      }
    />
  )
}
// PublicRoute.tsx


import React from "react";
import { Route, RouteProps, Redirect } from "react-router-dom";
import { useSelector } from "react-redux";

interface Props extends RouteProps {}

export const PublicRoute: React.FC<Props> = props => {
  const isAuthenticated: boolean = useSelector<any, any>((stores) => stores.auth.isAuthenticated);
  const { component: Component, ...restProps } = props;

  if (!Component) return null;

  return (
    <Route
      {...restProps}
      render={routeRenderProps => (
        !isAuthenticated ? (
          <Component {...routeRenderProps} />
        ) : (
          <Redirect
            to={{
              pathname: "/",
              state: { from: routeRenderProps.location }
            }}
          />
        )
      )}
    />
  )
}

// PageSettings.tsx

import React from "react";
import { LinkContainer } from "react-router-bootstrap";
import Button from "react-bootstrap/Button";
import {
  Switch,
  useRouteMatch,
  Redirect,
  Switch
} from "react-router-dom";

import { PrivateRoute } from "../../routes/PrivateRoute";
import { PageSettingsProfile } from "./profile";
import { PageSettingsBilling } from "./billing";
import { PageSettingsAccount } from "./account";

export const PageSettings = () => {
  const { path } = useRouteMatch();

  return (
    <div>
      <h2>Settings</h2>

      <Redirect strict from={path} to={`${path}/profile`} />

      <LinkContainer to={`${path}/profile`}>
        <Button>Profile</Button>
      </LinkContainer>
      <LinkContainer to={`${path}/billing`}>
        <Button>Billing</Button>
      </LinkContainer>
      <LinkContainer to={`${path}/account`}>
        <Button>Account</Button>
      </LinkContainer>

      <Switch>
        <PrivateRoute path={`${path}/profile`} component={PageSettingsProfile} />
        <PrivateRoute path={`${path}/billing`} component={PageSettingsBilling} />
        <PrivateRoute path={`${path}/account`} component={PageSettingsAccount} />
      </Switch>
    </div>
  );
};

Solution 4 - Reactjs

The component can be a function that returns JSX.

  <Route>
    <Route path="/" component={App}>
      <IndexRoute component={Home} />
      <Route path="Invite" component={()=>(<div><Home/><Invite/></div>)} />
    </Route>
  </Route>

Solution 5 - Reactjs

Instead of Cluttering so much, you can directly use two switch statements inside the Router tag.`

    <div className= {classes.root}>
      <CssBaseline></CssBaseline>
      <Router>
      <Switch>
        <Route path="/" exact component={Header}></Route>
        <Route path="/login" component={Login}></Route>
      </Switch>
      <Switch>
        <Route path="/" exact component={Checkout}></Route>
      </Switch>
      </Router> 
    </div>

This will solve your problem of having two components one below the other.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionTomView Question on Stackoverflow
Solution 1 - ReactjsknowbodyView Answer on Stackoverflow
Solution 2 - ReactjsSebastien HorinView Answer on Stackoverflow
Solution 3 - ReactjsJordy van den AardwegView Answer on Stackoverflow
Solution 4 - ReactjsAdam GeringView Answer on Stackoverflow
Solution 5 - Reactjsyash telkhadeView Answer on Stackoverflow