28

New to React:

I have a <Header /> Component that I want to hide only when the user visit a specific page.

The way I designed my app so far is that the <Header /> Component is not re-rendered when navigating, only the page content is, so it gives a really smooth experience.

I tried to re-render the header for every route, that would make it easy to hide, but I get that ugly re-rendering glitch each time I navigate.

So basically, is there a way to re-render a component only when going in and out of a specific route ?

If not, what would be the best practice to achieve this goal ?

App.js:

class App extends Component {

  render() {
    return (
      <BrowserRouter>
        <div className="App">
          <Frame>
            <Canvas />
            <Header />
            <Main />
            <NavBar />
          </Frame>
        </div>
      </BrowserRouter>
    );
  }
}

Main.js:

const Main = () => (
  <Switch>
    <Route exact activeClassName="active" path="/" component={Home} />
    <Route exact activeClassName="active" path="/art" component={Art} />
    <Route exact activeClassName="active" path="/about" component={About} />
    <Route exact activeClassName="active" path="/contact" component={Contact} />
  </Switch>
);
Yannick
  • 1,550
  • 4
  • 18
  • 27

6 Answers6

29

I'm new to React too, but came across this problem. A react-router based alternative to the accepted answer would be to use withRouter, which wraps the component you want to hide and provides it with location prop (amongst others).

import { withRouter } from 'react-router-dom';    
const ComponentToHide = (props) => {
  const { location } = props;
  if (location.pathname.match(/routeOnWhichToHideIt/)){
    return null;
  }

  return (
    <ComponentToHideContent/>
  )
}

const ComponentThatHides = withRouter(ComponentToHide);

Note though this caveat from the docs:

withRouter does not subscribe to location changes like React Redux’s connect does for state changes. Instead, re-renders after location changes propagate out from the component. This means that withRouter does not re-render on route transitions unless its parent component re-renders.

This caveat not withstanding, this approach seems to work for me for a very similar use case to the OP's.

Community
  • 1
  • 1
rwold
  • 2,216
  • 1
  • 14
  • 22
13

Since React Router 5.1 there is the hook useLocation, which lets you easily access the current location.

import { useLocation } from 'react-router-dom'

function HeaderView() {
  let location = useLocation();
  console.log(location.pathname);
  return <span>Path : {location.pathname}</span>
}
Yannick
  • 1,550
  • 4
  • 18
  • 27
robbit
  • 407
  • 3
  • 11
10

You could add it to all routes (by declaring a non exact path) and hide it in your specific path:

<Route path='/' component={Header} /> // note, no exact={true}

then in Header render method:

render() {
  const {match: {url}} = this.props;

  if(url.startWith('/your-no-header-path') {
    return null;
  } else {
    // your existing render login
  }
}
Meir
  • 14,081
  • 4
  • 39
  • 47
  • Thanks. I did edit my questions with more details... would that solution still apply ? – Yannick Jun 09 '18 at 19:12
  • Usually path='/' is a webapp's home page. I think it makes more sense for the component to be a Layout component and inside of that component should be the logic to display the header or not. – Isaac Pak Aug 17 '20 at 03:32
4

You can rely on state to do the re-rendering.

If you navigate from route shouldHide then this.setState({ hide: true })

You can wrap your <Header> in the render with a conditional:

{
  !this.state.hide &&
  <Header>
}

Or you can use a function:

_header = () => {
  const { hide } = this.state
  if (hide) return null
  return (
    <Header />
  )
}

And in the render method:

{this._header()}

I haven't tried react-router, but something like this might work:

class App extends Component {

  constructor(props) {
    super(props)
    this.state = {
      hide: false
    }
  }

  toggleHeader = () => {
    const { hide } = this.state
    this.setState({ hide: !hide  })
  }

  render() {

    const Main = () => (
      <Switch>
        <Route exact activeClassName="active" path="/" component={Home} />
        <Route
          exact
          activeClassName="active"
          path="/art"
          render={(props) => <Art toggleHeader={this.toggleHeader} />}
        />
        <Route exact activeClassName="active" path="/about" component={About} />
        <Route exact activeClassName="active" path="/contact" component={Contact} />
      </Switch>
    );

    return (
      <BrowserRouter>
        <div className="App">
          <Frame>
            <Canvas />
            <Header />
            <Main />
            <NavBar />
          </Frame>
        </div>
      </BrowserRouter>
    );
  }
}

And you need to manually call the function inside Art:

this.props.hideHeader()

Dan
  • 2,455
  • 3
  • 19
  • 53
  • I did edit my question with some code so the way I designed the layout and routing is more clear. Maybe the problem comes from that...So, I will have to update the app state from the component.. that is the one I want to hide the header... and then set the state to hide:false in every other route ? – Yannick Jun 09 '18 at 19:00
  • I am a bit unclear how you route here. Do you change the content somewhere? What happens when you click on the route? – Dan Jun 09 '18 at 19:07
  • First time I use Router... I decided to have all the page content in
    and switch the content with Router with `path="/" component={Home}` ... `path="/about" component={About}` etc etc ... that would work great if I didnt want to get rid of
    on just one specific route... so it's kind of a design problem as well I guess
    – Yannick Jun 09 '18 at 19:11
  • Can you link the Router package? I haven't used router before so this might not apply. But what you can do is to pass a function that modifies App's Header state to the route. And have the child call that function and directly modify the parent's header state. – Dan Jun 09 '18 at 19:16
  • I guess I'll have to look into the Context API or Redux to do this right? Well, maybe not Redux just for that... – Yannick Jun 09 '18 at 19:22
  • I looked at `react-router`. You can pass a function to it, so hopefully you can affect the parent this way. I updated my answer with how this might work, hopefully that'll help. – Dan Jun 09 '18 at 19:31
  • Almost there Dan... I have an infinite loop problem with this solution. When clicking on the Art link, the state update to 'hide', the component re-render and run the toggle function again, the state updates to '!hide' ... re-render ... etc etc.... how can I avoid this ? – Yannick Jun 24 '18 at 09:43
  • If possible, can you post your code? I haven't used router, so can you confirm that the constructor for Art is only triggered once on render? It shouldn't trigger multiple times when you click on Art. Another thing to try is to split the `toggleHeader` function into `hideHeader` and `showHeader` for specific routes. This should at least resolve the issue of it alternating between show and hide. But the main thing here might be to resolve and confirm that Art's `constructor` and `componentDidMount` runs only once on click. – Dan Jun 24 '18 at 19:00
  • constructor runs in infinite loop yes – Yannick Jun 28 '18 at 13:57
  • is there a way I can pass a parameter to this props ( false or true) and then update it on ComponentWillMount and ComponentWillUnmount lifecycle methods ? – Yannick Jun 28 '18 at 13:59
  • I think I am going to look into Redux or the context API to keep track of the state easier. I discovered the withRouter method that allow to keep track of the page visited... if I wrap the App with it it should be easy – Yannick Jun 28 '18 at 14:13
0
{location.pathname !== '/page-you-dont-want' && <YourComponent />}

This will check the path name if it is NOT page that you DO NOT want the component to appear, it will NOT display it, otherwise is WILL display it.

fruitloaf
  • 1,628
  • 15
  • 10
0

Visibility handler reusable approach

// VisibilityHandler.js
import { useEffect, useState } from "react";
import { useLocation } from "react-router-dom";

export const VisibilityHandler = ({children, notAllowedRoute}) => {

    const location = useLocation();
    const [isVisiable, setIsVisiable] = useState(null);

    const notEmtpy = arr => arr.some( el => el === null );
    const allowed = ( arr ) => {
        let res = notEmtpy(arr.map((path) => location.pathname.match(path) ));
        if(!res) return null
        return true
    }

    useEffect( ()=> {
        setIsVisiable(allowed(notAllowedRoute));
    }, [location, window.location])

    return (
        <>
          { isVisiable && children }
        </>
    )
}
// App.js
<VisibilityHandler notAllowedRoute={["dont-show-on-this-route", "/another-route"]}>
    <h1>Your content goes here</h1>
</VisibilityHandler>
GMKHussain
  • 3,342
  • 1
  • 21
  • 19