How to detect matched route from a component outside of the <Route/> component that was matched using react-router?

Viewed 1752

I've got the following structure in my React app, using react-router-dom.

<Router>
  <Header/>
  <Main>
    <AllRoutes>      // THIS HANDLES THE SWITCH WITH ALL THE ROUTES
      <Switch>
        <Route exact path={ROUTES.HOME} component={Home}/>
        <Route exact path={ROUTES.ABOUT} component={About}/>
        <Route exact path={ROUTES.PRIVACY} component={Privacy}/>
        // ETC
      </Switch>
    </AllRoutes>
  </Main>
  <Footer/>         // <==== FOOTER NEEDS TO KNOW WHICH ROUTE HAS BEEN MATCH
<Router>

QUESTION

Footer needs to know what <Route/> has been match. What is the best pattern to achieve that?


OPTION #1

I found the useRouteMatch hook over on react router docs:

enter image description here

enter image description here

This would kind of work, but I don't think it is good enough for my situation. Because a URL string can match a route and still don't be a valid route at the same time.

For example:

  • Route: /:language/privacy
  • Valid route: /en/privacy
  • Not valid route that would also match: /notALanguage/privacy

Once a route has match, I usually need to check if it is valid before rendering a component page or the 404 page.

Like:

<Route exact path={"/:language/privacy"} render={(routeProps) => {
 const possibleLanguage = routeProps.match.params.language;
 if (possibleLanguage in LANGUAGES) {
   return(
     <PrivacyPage lang={possibleLanguage}/>
   );
 }
 else {
  return(
    <Page404/>
  );
 }
}}/>

OPTION #2

What I'm thinking about doing is:

  • App.js calls useLocation. So it always re-render when there is a route change.
  • I could add a detectRoute function in App.js to do all the route checking beforehand.
  • And my AllRoutes component wouldn't need a component. I would implement a native JS switch and render the corresponding route.
  • This way I know upfront which <Route/> is going to match and I can pass it on to <Footer/> or any component that lives outside of the matched <Route/>.

Something like this:

SandBox Link

export default function App() {
  console.log("Rendering App...");

  const location = useLocation();

  // THIS WOULD BE THE detectRoute FUNCTION
  // I COULD EVEN USE THE useRouteMatch HOOK IN HERE
  const matchedRoute =
    location.pathname === ROUTE1
      ? "ROUTE1"
      : location.pathname === ROUTE2
      ? "ROUTE2"
      : "404";

  return (
    <div>
      <div className="App">
        <Link to={ROUTE1}>Route 1</Link>
        <Link to={ROUTE2}>Route 2</Link>
        <Link to={"/whatever"}>Route 404</Link>
      </div>
      <div>
        <AllRoutes matchedRoute={matchedRoute} />
      </div>
    </div>
  );
}

function AllRoutes(props) {
  switch (props.matchedRoute) {
    case "ROUTE1":
      return <Route exact path={ROUTE1} component={Page1} />;
    case "ROUTE2":
      return <Route exact path={ROUTE2} component={Page2} />;
    default:
      return <Route exact path={"*"} component={Page404} />;
  }
}

It works. But I would like to know if there's a proper way of doing this, 'cause this seems a bit weird and there might be something out there that was specifically designed for this.

2 Answers

Generally you want to either:

  1. Wrap the components together
  2. Create another switch to route them (and pass match params)

I put together a somewhat comprehensive example of the options. Hope that helps!

import React from "react";
import "./styles.css";
import { Switch, Link, Route, BrowserRouter } from "react-router-dom";

const hoc = (Component, value) => () => (
  <>
    <main>
      <Component />
    </main>
    <Footer value={value} />
  </>
);

const Wrapper = ({ component: Component, value }) => (
  <>
    <main>
      <Component />
    </main>
    <Footer value={value} />
  </>
);

const WrapperRoute = ({ component, value, ...other }) => (
  <Route
    {...other}
    render={props => <Wrapper component={component} value={value} {...props} />}
  />
);

const Footer = ({ value }) => <footer>Footer! {value}</footer>;
const Header = () => <header>Header!</header>;

const Another = () => <Link to="/onemore">One More!</Link>;
const Home = () => <Link to="/other">Other!</Link>;
const OneMore = () => <Link to="/">Home!</Link>;
const Other = () => <Link to="/another">Another!</Link>;

export default () => (
  <BrowserRouter>
    <Header />
    <Switch>
      {/* You could inline it! */}
      <Route
        path="/another"
        render={() => (
          <>
            <main>
              <Another />
            </main>
            <Footer value="" />
          </>
        )}
      />
      {/* You could use a custom route component (that uses an HOC or a wrapper) */}
      <WrapperRoute
        component={OneMore}
        path="/onemore"
        value="I got one more!"
      />
      {/* You could use a Higher-Order Component! */}
      <Route path="/other" component={hoc(Other, "I got other!")} />
      {/* You could use a wrapper component! */}
      <Route
        path="/"
        render={() => <Wrapper component={Home} value="I got home!" />}
      />
    </Switch>
    {/* You could have another switch for your footer (inline or within the component) */}
    <Switch>
      <Route
        path="/another"
        render={() => <Footer value="Switch footer another!" />}
      />
      <Route
        path="/other"
        render={() => <Footer value="Switch footer other!" />}
      />
      <Route
        path="/onemore"
        render={() => <Footer value="Switch footer onemore!" />}
      />
      <Route path="/" render={() => <Footer value="Switch footer home!" />} />
    </Switch>
  </BrowserRouter>
);

Note the WrapperRoute would allow you to do validation on your match params before passing them through. You could do a Redirect if needed.

What I've ended up doing:

Since I'm using Redux, I added a piece of global state to keep track of the matched route.

And I dispatch actions to update that state from the render prop from the <Route/>'s component.

<Switch>
  <Route key={index} exact path={"/some-route"} render={(routeProps) => {

    // HERE I DISPATCH AN ACTION TO CHANGE THE STATE FOR THE CURRENT ROUTE

    dispatch({
      type: UPDATE_CURRENT_ROUTE,
      payload: { name: "SOME_ROUTE_NAME" }
    });

    return (
      <PrivacyPage
        {...routeProps}
      />
    );

  }}/>
</Switch>

And now I can do on Footer.js:

function Footer() {

  const currentRoute = useSelector((state) => state.currentRoute);

  // RENDER FOOTER ACCORDINGLY TO THE CURRENT ROUTE
}
Related