React-router: How do you create a Master Detail page? (v4)

Created on 22 Sep 2016  Â·  22Comments  Â·  Source: ReactTraining/react-router

I went through the docs multiple times but I'm still not sure how to create a layout that can share some common components.

How do you create a layout that always has a header and footer, and only body changes? And how do you create more routes within that body? (bonus)

thanks

Most helpful comment

while this was pretty easy in previous versions

<Router history={browserHistory}>
  <Route path="/" component={MainLayout}>
    <IndexRoute component={Home} />
    <Route path="/about" component={About} />
    <Route path="/terms-of-service" component={TermsOfService} />
    <Route path="/faqs" component={Faqs} />
  </Route>
  <Route path="/" component={AdminLayout}>
    <IndexRoute component={Dashboard} />
    <Route path="/users" component={Users} />
  </Route>
</Router>

All 22 comments

the basic example does this, doesn't it?

Not exactly.

I want to do something like:

<Router>
  <div className={'app-container'}>
    <Switch>
      <DefaultLayout>
        <Route exact path={'/'} component={Dashboard} />
      </DefaultLayout>

      <EmptyLayout>
        <Route component={NotFound} />
      </EmptyLayout>
    </Switch>
  </div>
</Router>

Where the app uses a different container component based on which route matches.

EDIT:

This works, but I'm not sure it's the best solution:

const routes = [
  {
    exact: true,
    path: '/',
    component: Dashboard,
    layout: DefaultLayout,
  }, {
    path: '/advertisers',
    component: Advertisers,
    layout: DefaultLayout,
  },
];

const App = props => (
  <Router history={history}>
    <div className={'app-container'}>
      <Switch>
        {routes.map(route => (
          <Route path={route.path} key={route.path} exact={route.exact}>
            <route.layout>
              <route.component />
            </route.layout>
          </Route>
        ))}

        <Route>
          <EmptyLayout>
            <Route component={NotFound} />
          </EmptyLayout>
        </Route>
      </Switch>
    </div>
  </Router>
);

@LukeAskew Have you found a better solution so far? I've got the same issue here. The page layout is expected to contain a header and a sidebar in all cases except when it's 404. The trick with routes.map would work, but it would also generate multiple instances of the header and the sidebar, so when a user is switching between routes, the layout DOM goes through a deep refresh :–(

@ryanflorence how would you solve this? It'd be great to have this case covered in the docs.

const routes = [
  { path: '/one',
    component: One
  },
  { path: '/two',
    component: Two
  },
  { path: '/three',
    component: Three
  }
]

<Router>
  <div>
    <Switch>
      {routes.map(route => (
        <Route path={route.path} component={Header}/>
      ))}
    </Switch>
    <Switch>
      {routes.map(route => (
        <Route path={route.path} component={Sidebar}/>
      ))}
    </Switch>

    <Switch>
      {routes.map(route => (
        <Route path={route.path} component={route.component}/>
      ))}
      <Route component={NoMatch}/>
    </Switch>
  </div>
</Router>

There's a bug right now #4578 that will avoid the remounting of Header and Sidebar as the routes change, but does this do what you want?

@LukeAskew

const routes = [
  { path: '/one',
    Component: One,
    Layout: Layout1
  },
  { path: '/two',
    Component: Two,
    Layout: Layout2
  },
  { path: '/three',
    Component: Three,
    Layout: Layout2
  }
]

<Router>
  <div>
    <Switch>
      {routes.map({ path, Layout, Component } => (
        <Route path={route.path} render={(props) => (
          <Layout {...props}>
            <Component {...props}/>
          </Layout>
        )}/>
      ))}
    </Switch>
  </div>
</Router>

Thanks for your reply @ryanflorence – this is close to what I was looking for, assuming that the componets are reused. What would be good as well is to be able to control that top-level <div>, because someone might want to end up with something like this:

// /
<LayoutForPageWithContent>
  <Header />
  <Sidebar />
  <Home />
</LayoutForPageWithContent>

// /about
<LayoutForPageWithContent>
  <Header />
  <Sidebar />
  <About />
</LayoutForPageWithContent>

// /cart
<LayoutForPageWithContent>
  <Header />
  <Sidebar />
  <Cart />
</LayoutForPageWithContent>

// ...

// /404
<LayoutForPageWithError>
  <Error404 />
</LayoutForPageWithError>

__UPD:__ Header and Sidebar can be omitted, because they can be a part of <LayoutForPageWithContent />

@kachkaev

const routes = [
  { path: '/',
    exact: true,
    component: Home
  },
  { path: '/about',
    component: About,
  },
  { path: '/cart',
    component: Three,
  }
]

<Router>
  <Switch>
    {routes.map({ path, exact, component: Comp } => (
      <Route path={path} exact={exact} render={(props) => (
        <LayoutWithSidebarAndHeader {...props}>
          <Comp {...props}/>
        </LayoutWithSidebarAndHeader>
      )}/>
    ))}
    <Route component={Error404}/>
  </Switch>
</Router>

@ryanflorence this solution wont reuse LayoutWithSidebarAndHeader as far as I understand, so their states and fetched will be lost each time the url changes.

there's a bug in switch #4578

fixed by

b556060d7f17d008877f4b976e0ff092b833d81d
32a5bbb5871f5024d9c4e62cddfaa020fd5f7857
7a02cc3545ea27b039d0bc6ebb652dd9e06928ed

next beta release will have it fixed, you'll need to pull from github and build yourself for now.

I'm not sure if that bug is related. Reusing component={Sidebar} is one thing, because it's just a reference to a component type. But reusing render={(props) => (...)} inside map is probably a completely different story, because here we have subtrees of independently existing component instances, not just references to their names.

I don't see any solution in the current API, but I may be wrong. Looks like the only place the wrapper can be toggled is in <Switch />:

<Switch wrapperComponent={({whatever}) => (whatever ? LayoutWithSidebarAndHeader : LayoutForPageWithError)}>
  <Route exactly path="/" component={Home} />
  <Route exactly path="/about" component={About} />
  <Route exactly path="/cart" component={Cart} />
  <Route component={Error404} />
</Switch>

It's ugly, but at least it gives a chance to really reuse the layout in the situation above, which is not the same as reusing peer neighbours like Header and Sidebar. Once again, I can be totally wrong here.

It's not a different story. It's fixed on the v4 branch.

Just updated to v4.0.0-beta.7 and can confirm that shared layout wrappers are reused perfectly now! Checked this by rendering a random instanceId in the layout, which is came from here: componentDidMount() { this.isntanceId =instance ${Math.random()}`; }

Two caveats to share:

  • When running yarn add [email protected], it is important not to forget yarn add [email protected], because otherwise some weird context-errors will show up.

  • When looping through routes using map, do not assign key to them as this is done elsewhere, because otherwise the layout will not be reused:

{routes.map(({ path, exact, content: ContentComponent }) => (
  <Route
    // NO key={path} etc. here!
    path={path}
    exact={exact}
    render={(props) => (
      <Layout {...props}>
        <ContentComponent {...props} />
      </Layout>
  )}
  />
))}

Thanks for your previous responses @ryanflorence! I enjoy V4 API more and more!

__UPD:__ Absence of key was causing a react warning the browser console. I simply applied key={0} and the warning was gone, still leaving the routes workable. Not sure this is the best practice though.

@kachkaev you shouldn't be installing react-router yourself at all. react-router-dom re-exports everything from react-router that you need.

How the approach, is their any backlog issue if I'll try like this.
I want a couple of different layouts for Anonymous users, Logged in users, then Admin users, Addtional layout like Page Layouts.

<Switch>
  <Route exact path="/login" render= { ({ ...rest }) => (
    <Login { ...rest } onLogin={ (props) => { // do something on login } } />
  ) } />

  <Route exact path="/register" component={Register} />

  <Route path="/admin" render={ ({ ...rest }) => (
    <AuthLayout { ...rest }>
      <Switch>
        <Route exact path="/admin" render={ (props) => (
          <Dashboard />
        ) } />
      </Switch>
    </AuthLayout>
  ) } />

  <Route path="/">
    <MainLayout>
      <Switch>
        <Route exact path="/" >
          <Home />
        </Route>
        <Route exact path="/about" >
          <About />
        </Route>
        <Route exact path="/faqs" >
          <Faqs />
        </Route>
        <Route exact path="/terms-of-service" >
          <TermsOfService />
        </Route>
      </Switch>
    </MainLayout>
  </Route>
</Switch>

or how can I achieve this in a better way.

while this was pretty easy in previous versions

<Router history={browserHistory}>
  <Route path="/" component={MainLayout}>
    <IndexRoute component={Home} />
    <Route path="/about" component={About} />
    <Route path="/terms-of-service" component={TermsOfService} />
    <Route path="/faqs" component={Faqs} />
  </Route>
  <Route path="/" component={AdminLayout}>
    <IndexRoute component={Dashboard} />
    <Route path="/users" component={Users} />
  </Route>
</Router>

@ankibalyan Have you tried this:

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

@timdorr it does not seem that this issue should be closed. Based on the basic example one would need to create PrivateLayout, PublicLayout and in each of those layouts define the routes? Whereas in the older version you could just define everything in 1 file. In v4 it seems you can only go 1 level deep each time when defining a route/template?

I tried with this approach:

const DefaultLayout = ({children}) => (
  <div>
     <Header/>
     {children}
 </div>
);
<Router>
<Switch>
                <Route path="/checkout" exact component={Checkout}/>
                <DefaultLayout>
                    <Switch>
                        <Route path="/" exact component={Home}/>
                        <Route path="/products" component={Products}/>
                        <Route path="/product/:item" component={ProductSingle}/>
                        <Route path="/cart" component={Cart}/>
                        <Route path="/login"  component={Login}/>
                        <Route path="/register" component={Register}/>
                        <Route path="/forgetpass" component={ForgetPass}/>
                    </Switch>
                </DefaultLayout>
            </Switch>
</Router>

I used this approach

  <Provider store={store}>
    <ConnectedRouter history={history}> // this is from connected-react-router for my sagas 
      <Switch>
        {routes.map(({ path, exact, component: Component }) => (
          <Route
            key={path}
            path={path}
            exact={exact}
            render={props => (
              <App {...props}>
                <Component {...props} />
              </App>
            )}
          />
        ))}
      </Switch>
    </ConnectedRouter>
  </Provider>

And add side bar in App component with sidebars in routes.js
https://reacttraining.com/react-router/web/example/sidebar

Excellent !!!

@eashish93 Unfortunately there doesn't seem to be a way to make that approach work with a layoutless fallthrough (e.g. 404), or with 2 or more different layouts. It works nicely for some use cases though.

@kachkaev Thanks for the tip about not using keys. I was using keys and components were getting remounted on every route change. What's the best practice for .map() to generate <Route> components in terms of assigning keys?

Was this page helpful?
0 / 5 - 0 ratings

Related issues

wzup picture wzup  Â·  3Comments

ackvf picture ackvf  Â·  3Comments

ryansobol picture ryansobol  Â·  3Comments

andrewpillar picture andrewpillar  Â·  3Comments

Waquo picture Waquo  Â·  3Comments