Now that React Router 4 allows us to nest routes inside other components, I keep running into an issue with resolving the not found page.
Say we have a component tree like:
// Top-level
<App>
<Switch>
<Route path="/account" component={Account}/>
//... other routes here
<Route component={NotFound}/>
</Switch>
</App>
// Nested in the component tree
<Account>
<div>Some shared content among all account pages</div>
<Switch>
<Route path="/account" component={MyAccount} exact/>
<Route path="/account/register" component={Register} exact/>
<Route path="/account/login" component={Login} exact/>
<Route path="/account/password/reset" component={ResetPassword} exact/>
<Route path="/account/password/forgot" component={ForgotPassword} exact/>
// How to handle NotFound here?
</Switch>
<div>Some more shared content among all account pages</div>
</Account>
It's rather difficult to handle a nested not found. At it stands, I put the shared stuff into their own components and then reuse them in every component in the "Account" section. It's not a terrible solution, but it's a bit verbose.
We could benefit from a set of <Throw/> and <Catch/> components. Of course React's data flow is unidirectional and for this to work, we have to use a callback on the context to indicate further up that we couldn't proceed with the render.
Another situation that this could come in handy is when a nested component has to make a data request to the server before it can render, but once it does the request, it finds out that the url is invalid and the resource doesn't exist and so it has to show a not found page.
Anyway, I was curious to know if any one else has come across this issue of a nested not found and if there's anything that can be done at the router level to make this less of a pain point. Thanks.
I am struggling with the same problem and haven't found any good solution. To use a redirect here seems a bit wrong because I am losing the url in the browser.
If you're using something like redux or mobx, you could pop a thing on the state tree that the top level component reads to switch the view into your NotFound component.
But how you handle that is entirely up to you. Switch handles that more as a "default" case, rather than a "no match" case. So mapping the functionality to that use case doesn't exactly work.
Sorry for opening this up again.
Would this be a valid solution?
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter, Switch, Route, Link } from 'react-router-dom';
const Home = () => <div>Home</div>
const User = () => <div>User</div>
// Lock down route to escape frontend component
class EscapeErrorRoute extends Component {
componentWillMount() {
const { lockdownRoute } = this.props;
lockdownRoute();
}
render() {
return <div>You shouldn't see this</div>
}
}
// Global error component
class Error extends Component {
componentWillReceiveProps(nextProps) {
const { location, unlockRoute } = this.props;
const { location: nextLocation } = nextProps;
if (location.pathname !== nextLocation.pathname) {
unlockRoute();
}
}
componentWillUnmount() {
// Required for navigating back to root route.
const { unlockRoute } = this.props;
unlockRoute();
}
render() {
return <div><h1>Global 404</h1></div>
}
}
// Frontend component
const Frontend = (props) => {
const { lockdownRoute } = props;
return (
<div>
<h1>Frontend</h1>
<div>
<p><Link to="/">Home</Link></p>
<p><Link to="/user">User</Link></p>
<p><Link to="/not-found-route">Error</Link></p>
</div>
<hr />
<Switch>
<Route exact path="/" component={Home} />
<Route path="/user" component={User} />
<Route render={ (props) => <EscapeErrorRoute {...props} lockdownRoute={lockdownRoute} /> } />
</Switch>
</div>
)
}
// Main app
class App extends Component {
constructor() {
super();
this.lockdownRoute = this.lockdownRoute.bind(this);
this.unlockRoute = this.unlockRoute.bind(this);
this.state = {
lockdown: false
}
}
lockdownRoute() {
this.setState({
lockdown: true
})
}
unlockRoute() {
this.setState({
lockdown: false
})
}
render() {
const { lockdown } = this.state;
return (
<div>
<p>{ lockdown ? 'Route locked down' : 'Route opened' }</p>
<Switch>
<Route exact={lockdown} path="/" render={(props) => <Frontend {...props} lockdownRoute={this.lockdownRoute} unlockRoute={this.unlockRoute} />} />
<Route render={(props) => <Error {...props} unlockRoute={this.unlockRoute} />} />
</Switch>
</div>
)
}
}
ReactDOM.render(
<BrowserRouter><App /></BrowserRouter>,
document.getElementById('root')
);
It's a new idea, but you'd have a nested NotFound. You know enough to show the account page, but nothing from there.
I actually just redirect to the best route that makes sense:
<Switch>
<Route path="/account" component={MyAccount} exact/>
<Route path="/account/register" component={Register} exact/>
<Route path="/account/login" component={Login} exact/>
<Route path="/account/password/reset" component={ResetPassword} exact/>
<Route path="/account/password/forgot" component={ForgotPassword} exact/>
{/* redirect, seems more useful than "a not found" message */}
<Redirect to="/account"/>
</Switch>
Or you render another NotFound there, you knew enough to match /account, so render what you know, then "not found" what you don't.
As for how to get up to the parent page and render a "global not found" I've been meaning to make an example of that, it's a little bit round about. You'd redirect to the current path, but put some state on it, then in the parent you'd have a route that checks the state and if it's a "not found" then it doesn't even render the root <Switch>, just not found. Same idea as the modal example: https://reacttraining.com/react-router/web/example/modal-gallery
@ryanflorence Thanks for taking your time :).
Hey guys, so today I've came across this exact same issue and I was able to get something working to render a "global not found" component, while keeping the benefits of sharing some UI within a "parent" route. I wanted to share this with you, in case you find this useful for your case :)
Consider the routes I need to support:
| /
| /profile/:userId # General profile page
| /profile/:userId/settings # Settings within Profile
My goal was to render a global 404 (a not a nested 404) if trying to reach /profile/:userId/whatever or /profile/:userId/settings/whatever. I also wanted to keep the fact that route component shouldn't be path specific (so I can move them around, or use them for multiple routes).
I'll share the code snippet and maybe write some explanations after.
// App.js (or the root component)
/**
* Interpolate a string which holds variables (i.e :userId) and replace all occurrences
* by the values held in the `params` object.
*
* Given the string `/profile/:userId` and the params { userId: 'thomas' }
* it returns `/profile/thomas`.
*/
function interpolateParams(string, params = {}) {
return string.replace(/:([a-zA-Z]+)/g, (match, token1) => `${params[token1]}`);
}
/**
* Conditionally render a component for a base route and all of its sub routes.
* If any sub route is valid, we render the same base component, otherwise a 404.
*/
const RouteWithSubRoutes = (initialProps) => (
<Route path={initialProps.path} render={(props) => {
const validRoutes = [initialProps.path, ...initialProps.subRoutes]
.map(route => interpolateParams(route, props.match.params));
return validRoutes.includes(props.location.pathname)
? <initialProps.baseComponent {...props} />
: <Error404 />
}} />
);
<Switch>
<Route exact={true} path="/" component={Home} />
<RouteWithSubRoutes
path="/profile/:userId"
baseComponent={Profile}
subRoutes={[
'/profile/:userId/settings'
]}
/>
<Route component={Error404}/>
</Switch>
// in Profile.js
const Profile = ({ match }) => (
<div>
<h1>{match.params.userId} profile!</h1>
<p>This is some UI shared between all sub routes within Profile.</p>
<Link to={`${match.url}/settings`}>See settings</Link>
<Switch>
<Route exact={true} path={match.url} render={() => <div>Root Profile Page</div>} />
<Route exact={true} path={`${match.url}/settings`} render={props => <div>Profile Settings</div>} />
</Switch>
</div>
));
So this logic will match /profile/thomas and /profile/thomas/settings but won't match anything else. If a no match is found, a global 404 is rendered even inside a nested route.
@Magellol Your solution would work. But it seems we have to declare all routes in a single file as react-router v3.
@jacobdam Well kind of yeah. I mean, you'd have to define the parent route and all paths for the children routes as well for sure.
I wasn't able to come up with more separation of concerns for this as I needed to see if a global 404 needed to be rendered early enough in the render pipeline (before going too deep in the component tree)
@Magellol I guess that instead of implementing the interpolateParams function yourself you could use the matchPath function from react-router-dom.
@vanpacheco Yeah, it looks like I could use that to simplify a bit the matching logic. Thanks for pointing that out 馃憤
I've made a simple example demonstrating a nested "not found" page, maybe it will help some people in this thread.
See: https://github.com/ReactTraining/react-router/issues/4698#issuecomment-314419439
That's an interesting approach! I guess it goes more into the dynamic aspect of react-router 4.
The only thing that tickles me a bit is that you have to repeat the <RouteNotFound /> on each sub routes as well instead of handling it once but pretty sure you'd be able to abstract that Switch away to always append a <RouteNotFound /> at the end.
Most helpful comment
Hey guys, so today I've came across this exact same issue and I was able to get something working to render a "global not found" component, while keeping the benefits of sharing some UI within a "parent" route. I wanted to share this with you, in case you find this useful for your case :)
Consider the routes I need to support:
My goal was to render a global 404 (a not a nested 404) if trying to reach
/profile/:userId/whateveror/profile/:userId/settings/whatever. I also wanted to keep the fact that route component shouldn't be path specific (so I can move them around, or use them for multiple routes).I'll share the code snippet and maybe write some explanations after.
So this logic will match
/profile/thomasand/profile/thomas/settingsbut won't match anything else. If a no match is found, a global 404 is rendered even inside a nested route.