7

I searched a lot on the Web and SO, asked in reactiflux chat, but didn't found a clean and non-hack-looking way of rendering some component depending of route/path.

Let's say I have the <Header /> which should be shown on some pages and should be hidden on other ones.

For sure I can use this string in Header component

if (props.location.pathname.indexOf('/pageWithoutAHeader') > -1) return null

That's totally fine, if /pageWithoutAHeader is the unique. If I need same functionality for 5 pages it become this:

if (props.location.pathname.indexOf('/pageWithoutAHeader1') > -1) return null
if (props.location.pathname.indexOf('/pageWithoutAHeader2') > -1) return null
if (props.location.pathname.indexOf('/pageWithoutAHeader3') > -1) return null

Yes, I can store routes in the array and write a loop, which will be more code-reusable. But is it a best and the most elegant way to handle this use case?

I believe that it can be even buggy, for example if I don't render header for a page with a route /xyz and I have routes with UUIDs, like /projects/:id, and id=xyzfoo, so /projects/xyzfoo won't show a header but it should.

6 Answers 6

7

In order to achieve DRY rule(avoid code repetition) and implements the conditional rendering depending on routes, you should work on the following structure:

step 1) Create the layout(HOC) which returns the given component with the <Header/> and export it

import React from "react"
import { Route } from "react-router-dom"
import Header from "./Header"

export const HeaderLayout = ({component: Component, ...rest}) => (
    <Route {...rest} render={(props) => (
        <>
            <Header/>
            <Component {...props} />
        </>
    )} />
)

Step 2) import layout and use it

import React, { Component } from 'react'
import { BrowserRouter, Route, Switch } from "react-router-dom"
import Test1 from './Test1';
import Test2 from './Test2';
import { HeaderLayout } from './HeaderLayout';

export default class Main extends Component {
    render() {
        return (
            <BrowserRouter>
                <Switch>
                    <HeaderLayout path="/test1" component={Test1} />
                    <Route path="/test2" component={Test2}/>
                </Switch>
            </BrowserRouter>

        )
    }
}

Output :

path-test1

path-test2

Conclusion :

So, whenever you want to include header component along with your route defined component use <HeaderLayout /> and if you don't want to use header then simply use <Route /> to hide header in your page.

Sign up to request clarification or add additional context in comments.

Comments

6
+50

You can list all routes without a header first and group others in additional switch:

...
<Switch>
  <Route path="/noheader1" ... />
  <Route path="/noheader2" ... />
  <Route path="/noheader3" ... />
  <Route component={HeaderRoutes} />
</Switch>
...

HeaderRoutes = props => (
  <React.Fragment>
    <Header/>
    <Switch>
      <Route path="/withheader1" ... />
      <Route path="/withheader2" ... />
      <Route path="/withheader3" ... />
    </Switch>
  </React.Fragment>
)

From the documentation:

Routes without a path always match.

Unfortunately this solution might have a problem with "not found" page. It should be placed at the end of the HeaderRoutes and will be rendered with a Header.

Dhara's solution doesn't have such problem. But it might not work well with Switch if React Router internals change:

All children of a <Switch> should be <Route> or <Redirect> elements. Only the first child to match the current location will be rendered.

HOC over Route is not a Route itself. But it should work fine because current codebase in fact expects any React.Element with the same props semantics as <Route> and <Redirect> have.

4 Comments

Wow, I have to check this, and if it works like that it is exactly what I need! :)
I checked and it works, thank you! This is the most elegant way without any code repetition. And I am surprised on how it works, I will appreciate an explanation on how we used Route without path prop
Thanks for explanation! I like Dhara's solution too(and jdc91's one, he came first with HOC) but I found your one the less code-repetitive one.
You can also use the prop path="*" though it seems a bit redundant.
4

I think you are looking at this the wrong way. So composability is the number one trait when thinking in React. A header is a reusable component which can be dropped anywhere you want!

Thinking in this way will provide you with multiple options.

Let's say you have several page routes that you have designed for your application. A header is a child component of any of those pages who use it!

function AppRouter() {
  return (
    <Router>
      <div>
        <nav>
          <ul>
            <li>
              <Link to="/">Home</Link>
            </li>
            <li>
              <Link to="/about/">About</Link>
            </li>
            <li>
              <Link to="/users/">Users</Link>
            </li>
          </ul>
        </nav>

        <Route path="/" exact component={Index} />
        <Route path="/about/" component={About} />
        <Route path="/users/" component={Users} />
      </div>
    </Router>
  );
}

Now within each page you want the header you can go and just introduce the Header Component wherever necessary.

export default function Index(){
    return (
        <React.Fragment>
             <Header/>
             <div> ... Index Content </div>
        </React.Fragment>
    );
}

export default function About(){
    return (
        <React.Fragment>
             //I don't need a header here.
             <div> ... Index Content </div>
        </React.Fragment>
    );
}

An even more elegant but a bit more complex approach would be to introduce a Higher order component. This would make your intentions more clear on adding headers at the route level!

function withHeader(Page){
    return class extends React.Component {
        render() {
          // Wraps the input component in a container, without mutating it.
          return (
              <React.Fragment>
                 <Header/>
                 <Page {...this.props} />);
              </React.Fragment>
        }
    }
}

function AppRouter() {
  return (
    <Router>
      <div>
        <nav>
          <ul>
            <li>
              <Link to="/">Home</Link>
            </li>
            <li>
              <Link to="/about/">About</Link>
            </li>
            <li>
              <Link to="/users/">Users</Link>
            </li>
          </ul>
        </nav>

        <Route path="/" exact component={withHeader(Index)} />
        <Route path="/about/" component={About} />
        <Route path="/users/" component={Users} />
      </div>
    </Router>
   );
}

1 Comment

importing Header and using it everywhere looks like a big code repetition to me, which doesn't solve a problem of making code less repeatable.. HOC looks a bit better, but it still occur some repeatable parts. Any chance to do the same, but wrap multiple <Route> elements with something so every route inside that wrapper will render components with Header ?
2

Include this data about the header as a route parameter or a query.

/projects/:headerBool/:id

Or:

/projects/:id?header=false

Then you can access it via props.match or props.location.

2 Comments

I can't say if it is a best solution, because I don't want to store Header "state" in route + anyone could just replace it with what he need.
True! Well, you can decrease the likelihood of it bugging out by splitting the pathname by '/' and then looking for exact match for each part. And if you do it that way it makes more sense to make an object with keys the strings you're looking for and values 'true' so you can just look up each part as you iterate through them.
0

You can use render attribute of the Route. Example:

<Route path='/pageWithoutHeader' render={() => <Page1 />} />
<Route path='pageWithHeader' render={() => 
      <Header/>
      <Page2 />}
/>

This method is better than using the header component inside the Page.

Comments

0

A scenario that often pops up for me is certain pages in the database having Header = false or Page Title = false. A good solution for that would be a Context.

import React, { createContext, useContext, useState, useEffect } from "react";
import { Switch, Route } from "react-router-dom";

const AppContext = createContext({});

const Header = () => {
  const { headerActive } = useContext(AppContext);

  if (!headerActive) {
    return null;
  }

  return (
    <header>I'm a header</header>
  );
}

const PageWithHeader = () => {
  const { setHeaderActive } = useContext(AppContext);
  useEffect(() => {
    setHeaderActive(true);
  },[setHeaderActive]);

  return (
    <div>Page with header</div>
  );
}

const PageWithoutHeader = () => {
  const { setHeaderActive } = useContext(AppContext);
  useEffect(() => {
    setHeaderActive(false);
  },[setHeaderActive]);

  return (
    <div>Page without header</div>
  );
}

export const App = () => {
  const [headerActive, setHeaderActive] = useState(true);

  return (
    <AppContext.Provider value={{ headerActive, setHeaderActive }}>
      <Header />
      <Switch>
        <Route path="page-1">
          <PageWithHeader />
        </Route>
        <Route path="page-2">
          <PageWithoutHeader />
        </Route>
      <Switch>
    </AppContext.Provider>
  );
}

You could also simplify it further by using a custom hook. Something like:

export const useHeader = (active) => {
  const { headerActive, setHeaderActive } = useContext(AppContext);
  useEffect(() => {
    if (active !== undefined) {
      setHeaderActive(active);
    }
  },[setHeaderActive, active]);

  return headerActive;
}

Then in the original components:

const Header = () => {
  const headerActive = useHeader();

  if (!headerActive) {
    return null;
  }

  return (
    <header>I'm a header</header>
  );
}
...
const PageWithoutHeader = () => {
  useHeader(false);

  return (
    <div>Page without header</div>
  );
}

You could also get fancy and use the useLocation hook paired with useRef to keep track of the previous and current pathnames so you can have a default state without needing to declare useHeader in every page.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.