2

I'm trying to render most of my routes as children of an AppShell component, which contains a navbar. But I want to render my 404 route as a standalone component not wrapped in AppShell.

It was easy with v2:

<Router>
  <Route component={AppShell}>
    <Route path="/about" component={About} />
    <Route path="/" component={Home} />
  </Route>
  <Route path="*" component={NotFound} />
</Router>

Everything works as desired:

  • / renders <AppShell><Home /></AppShell>
  • /about renders <AppShell><About /></AppShell>
  • /blah renders <NotFound />

But I can't figure out how to do it with v4:

Right now I'm doing this, but the problem is it renders AppShell (with no children, but still a navbar):

const Routes = () => (
  <div>
    <AppShell>
      <Match exactly pattern="/" component={Home} />
      <Match pattern="/about" component={About} />
    </AppShell>
    <Miss component={NotFound} />
  </div>
)

With this:

  • / renders <div><AppShell><Home /></AppShell></div> (good)
  • /about renders <div><AppShell><About /></AppShell></div> (good)
  • /blah renders <div><AppShell /><NotFound /></div> (problem -- I want to get rid of the <AppShell />)

Using an array pattern works if there's no root route:

const InAppShell = (): React.Element<any> => (
  <AppShell>
    <Match pattern="/about" component={About} />
    <Match pattern="/contact" component={Contact} />
  </AppShell>
)

const App = (): React.Element<any> => (
  <div>
    <Match pattern={['/contact', '/about']} component={InAppShell} />
    <Miss component={NotFound} />
  </div>
)

And using an array pattern with exactly works with the root route:

But then I have to put all possible child routes in the pattern array...

const InAppShell = (): React.Element<any> => (
  <AppShell>
    <Match exactly pattern="/" component={Home} />
    <Match pattern="/about" component={About} />
  </AppShell>
)

const App = (): React.Element<any> => (
  <div>
    <Match exactly pattern={["/", "/about"]} component={InAppShell} />
    <Miss component={NotFound} />
  </div>
)

But that would be pretty unwieldly in a large app with a bunch of routes.

I could make a separate Match for /:

const InAppShell = (): React.Element<any> => (
  <AppShell>
    <Match exactly pattern="/" component={Home} />
    <Match pattern="/about" component={About} />
    <Match pattern="/contact" component={Contact} />
  </AppShell>
)

const App = (): React.Element<any> => (
  <div>
    <Match exactly pattern="/" component={InAppShell} />
    <Match pattern={["/about", "/contact"]} component={InAppShell} />
    <Miss component={NotFound} />
  </div>
)

But this would remount <AppShell> every time I transition to and from the home route.

There doesn't seem to be an ideal solution here; I think this is a fundamental API design challenge v4 will need to solve.

If only I could do something like <Match exactlyPattern="/" pattern={["/about", "/contact"]} component={InAppShell} />...

Andy
  • 7,885
  • 5
  • 55
  • 61

3 Answers3

13

I've been working around the same issue - I think you may want something like this, a 'global 404':

https://codepen.io/pshrmn/pen/KWeVrQ

const {
  HashRouter,
  Route,
  Switch,
  Link,
  Redirect
} = ReactRouterDOM

const Global404 = () => (
  <div>
    <h1>Oh, no!</h1>
    <p>You weren't supposed to see this... it was meant to be a surprise!</p>
  </div>
)

const Home = () => (
  <div>
    The links to "How?" and "Random" have no matching routes, so if you click on either of them, you will get a "global" 404 page.
  </div>
)
const Question = ({ q }) => (
  <div>
    <div>Question: {q}</div>
    <div>Answer: I have no idea</div>
  </div>
)
const Who = () => <Question q={"Who?"}/>
const What = () => <Question q={"What?"}/>
const Where = () => <Question q={"Where?"}/>
const When = () => <Question q={"When?"}/>
const Why = () => <Question q={"Why?"}/>

const RedirectAs404 = ({ location }) => 
  <Redirect to={Object.assign({}, location, { state: { is404: true }})}/>

const Nav = () => (
  <nav>
    <ul>
      <li><Link to='/'>Home</Link></li>
      <li><Link to='/faq/who'>Who?</Link></li>
      <li><Link to='/faq/what'>What?</Link></li>
      <li><Link to='/faq/where'>Where?</Link></li>
      <li><Link to='/faq/when'>When?</Link></li>
      <li><Link to='/faq/why'>Why?</Link></li>
      <li><Link to='/faq/how'>How?</Link></li>
      <li><Link to='/random'>Random</Link></li>
    </ul>
  </nav>
)

const App = () => (
  <Switch>
    <Route exact path='/' component={Home}/>
    <Route path='/faq' component={FAQ}/>
    <Route component={RedirectAs404}/>
  </Switch>
)

const FAQ = () => (
  <div>
    <h1>Frequently Asked Questions</h1>
    <Switch>
      <Route path='/faq/who' component={Who}/>
      <Route path='/faq/what' component={What}/>
      <Route path='/faq/where' component={Where}/>
      <Route path='/faq/when' component={When}/>
      <Route path='/faq/why' component={Why}/>
      <Route component={RedirectAs404}/>
    </Switch>
  </div>
)

ReactDOM.render((
  <HashRouter>
    <div>
      <Nav />
      <Route render={({ location }) => (
        location.state && location.state.is404
          ? <Global404 />
          : <App />
      )}/>
    </div>
  </HashRouter>
), document.getElementById('root'))
bjackson2
  • 139
  • 1
  • 3
  • 1
    This is actually genius... It took me a while to understand just how genius this is... – Jan Paepke Oct 25 '19 at 07:42
  • How is this different from just adding something like in every Switch throughout the app? – user1056585 Sep 12 '20 at 21:22
  • The difference is that this renders the 404 page on the root component instead of Switch, see the code near the end `location.state && location.state.is404 ? : ` – Idrizi.A Nov 02 '20 at 18:52
1

Probably my least favorite thing about v4 is that matches and misses that should be grouped together can be placed on separate levels in the component tree. This leads to situations like yours where you have a component that should only be rendered for certain matches, but the multi-level match structure allows you to nest matches in it.

You should just render the <AppShell> as a container for each component that requires it.

const Home = (props) => (
  <AppShell>
    <div>
      <h1>Home</h1>
    </div>
  </AppShell>
)

const About = (props) => (
  <AppShell>
    <div>
      <h1>About</h1>
    </div>
  </AppShell>
)

const App = () => (
  <div>
    <Match exactly pattern='/' component={Home} />
    <Match pattern="/about" component={About} />
    <Miss component={NotFound} />
  </div>
)

You could also use the <MatchRoutes> component. I prefer this because it forces related routes to be grouped together.

const App = () => (
  <MatchRoutes missComponent={NotFound} routes={[
    { pattern: '/', exact: true, component: Home },
    { pattern: '/about', component: About }
  ]} />
)
Paul S
  • 35,097
  • 6
  • 41
  • 38
  • Unfortunately though this remounts the `AppShell` each time the route changes. I didn't know about `MatchRoutes` though, maybe I could get that to work well enough... – Andy Dec 01 '16 at 19:10
  • If my understanding of reconciliation is correct, with `` the `` shouldn't be un- and re-mounted. – Paul S Dec 01 '16 at 19:17
  • Okay. I'm hoping I can figure out a way to use `` without wrapping each component individually in ``... – Andy Dec 01 '16 at 19:37
  • If worst comes to worst I can write my own component that does the necessary matching on the location – Andy Dec 01 '16 at 19:37
  • Okay yes, looking at the code for `MatchRoutes`, I see that it won't remount ``. Also I could easily make a component that renders a `` with a `render` on each route that wraps the component in ``. – Andy Dec 01 '16 at 19:40
0

I came up with a custom <Match> component that is one possible solution. It's far from being a standard agreed-upon way of doing things though.

https://gist.github.com/jedwards1211/15140b65fbeafcbc14dec728fee16f59

Usage looks like

const InAppShell = (): React.Element<any> => (
  <AppShell>
    <Match exactPattern="/" component={Home} />
    <Match pattern="/about" component={About} />
    <Match pattern="/contact" component={Contact} />
  </AppShell>
)

const App = (): React.Element<any> => (
  <div>
    <Match exactPattern="/" patterns={["/about", "/contact"]} register={false} component={InAppShell} />
    <Match notExactPattern="/" notPatterns={["/about", "/contact"]} register={false} component={NotFound} />
  </div>
)
Andy
  • 7,885
  • 5
  • 55
  • 61