react-router v4: how to avoid rendering parent routes when child routes miss

此生再无相见时 提交于 2019-12-11 08:25:07

问题


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} />...


回答1:


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'))



回答2:


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 }
  ]} />
)



回答3:


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>
)


来源:https://stackoverflow.com/questions/40917527/react-router-v4-how-to-avoid-rendering-parent-routes-when-child-routes-miss

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!