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

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?

111358 次浏览

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/

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>

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>

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


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.

As of React Router v6, all the current answers are out of date, and this is much easier now.

Basic layout and auth examples are in the docs: https://reactrouter.com/docs/en/v6/examples/basic

Relevant piece of code below - the Outlet element is used to insert elements which are defined in the Route. As far as I can tell, there's only 1 Outlet that can be used per Route unfortunately.

function Dashboard() {
return (
<div>
<h1>Dashboard</h1>


{/* This element will render either <DashboardMessages> when the URL is
"/messages", <DashboardTasks> at "/tasks", or null if it is "/"
*/}
<Outlet />
</div>
);
}


function App() {
return (
<Routes>
<Route path="/" element={<Dashboard />}>
<Route
path="messages"
element={<DashboardMessages />}
/>
<Route path="tasks" element={<DashboardTasks />} />
</Route>
</Routes>
);
}

The shortest and most simplest way to do it is by:

https://medium.com/how-to-react/add-an-active-classname-to-the-link-using-react-router-b7c350473916

Add this code on your css file:

ul {
list-style: none;
display: flex;
justify-content: space-around;
}
ul li a{
text-decoration: none;
background-color: #000;
color: #fff;
padding: 10px 20px;
font-weight: bold;
}


ul li a:hover{
background-color: red;
color: #fff;
}


.active{
background-color: red;
color: #fff;
}

And this on your index.js:

            <ul>
<li><NavLink exact activeClassName="active" to='/'>Home</NavLink></li>
<li><NavLink activeClassName="active" to='/about'>About</NavLink></li>
<li><NavLink activeClassName="active" to='/service/inner'>Service</NavLink></li>
</ul>

activeClassName="active" makes it the class for the element if the to='/' is active

However if the class is not active it will use the default which is ul