· react

React React Router: Setting parent component state based on route change event

I’ve been working on a React Reach Router based application that has several routes and wanted to show a search box in the header unless the user was on the search page. After a lot of trial and error I learnt that I could use a route change event listener to do this.

The CodeSandbox below shows all the code to do this:

Let’s walk through the code. We have a top level component called App that has the state showSearchBox, which defaults to true:

import React, {Component} from 'react';
import {globalHistory, Link, Router} from "@reach/router";

class App extends Component {
  constructor(props) {
    super(props)
    this.state = {
      showSearchBox: true
    };
  }
}

In our render function we display an input box only if this value is set to true:

class App extends Component {
  render() {
    return (
      <div className="App">
        <header className="App-header">
          <p>
            React Reach Router Demo
          </p>
          <div>
            {this.state.showSearchBox && <input type={"text"} style={{margin: "10px"}}/>}
          </div>
        </header>
        <nav style={{padding: "5px"}}>
          <Link to="/">Home</Link> <Link to="dashboard">Dashboard</Link> <Link to="search">Search</Link>
        </nav>
        <div style={{paddingTop: "10px"}}>
          <Router>
            <Home path="/"/>
            <Dashboard path="/dashboard"/>
            <Search path="/search"/>
          </Router>
        </div>
      </div>
    );
  }
}

In this code sample we can also see that we have 3 paths:

  • /

  • /dashboard and

  • /search

We want to set showSearchBox to false if we’re on the /search route, but set to true on any of the other routes.

I initially tried to control this state from the child components, without much success. The closest I got was a maximum update depth exceeded error, which wasn’t great.

By chance I came across a GitHub issue created by Marvin Heilemann, in which he asked whether there was a hook to capture route change events. And indeed there is.

We can update our App component to listen to these events and update the showSearchBox state by adding the following functions:

class App extends Component {
  componentDidMount() {
    this.toggleSearchBox(globalHistory.location)

    this.historyUnsubscribe = globalHistory.listen(({action, location}) => {
      if (action === 'PUSH') {
        this.toggleSearchBox(location)
      }
    });
  }

  componentWillUnmount() {
    this.historyUnsubscribe();
  }

  toggleSearchBox(location) {
    if (location.pathname === "/search") {
      this.setState({
        showSearchBox: false
      })
    } else {
      this.setState({
        showSearchBox: true
      })
    }
  }
}

In componentDidMount we also make sure that we toggle the search box based on the URL that we start on as well, otherwise it would default to true.

If we try the CodeSandbox provided at the top of the post we’ll see the following if we click the Dashboard link in the header:

dashboard

As expected, the search box is still showing. But if we click the Search link, we’ll see the following screen:

search

After I’d got this working I came across another GitHub issue, where Martin Mende showed how to achieve the same thing using state and effect hooks. The following code does the same thing as the App component that we defined above:

import React, {useEffect, useState} from 'react';
import {globalHistory, Link, Router} from "@reach/router";

function App() {
  const initialState = true;
  const [showSearchBox, setShowSearchBox] = useState(initialState);
  useEffect(() => {
    const removeListener = globalHistory.listen(params => {
      const { location } = params;
      const newState = location.pathname !== "/search";
      setShowSearchBox(newState);
    });
    return () => {
      removeListener();
    };
  }, []);

  return (
    <div className="App">
      <header className="App-header">
        <p>React Reach Router Demo</p>
        <div>
          {showSearchBox && <input type={"text"} style={{ margin: "10px" }} />}
        </div>
      </header>
      <nav style={{ padding: "5px" }}>
        <Link to="/">Home</Link> <Link to="dashboard">Dashboard</Link>{" "}
        <Link to="search">Search</Link>
      </nav>
      <div style={{ paddingTop: "10px" }}>
        <Router>
          <Home path="/" />
          <Dashboard path="/dashboard" />
          <Search path="/search" />
        </Router>
      </div>
    </div>
  );
}
  • LinkedIn
  • Tumblr
  • Reddit
  • Google+
  • Pinterest
  • Pocket