react-router v4: how to avoid rendering parent routes when child routes miss - http-status-code-404

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

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

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

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

Related

React-router-dom: Route not displaying expected results

js, routing to different modules.
Here is the code:
//Dashboard side bar navigation
const Navigation = () =>{
return (
<Navbar className="sidebar">
<ul className="list-unstyled">
<li><NavLink to="/dashboard" activeClassName='active' className="nav-link">Dashboard</NavLink></li>
<li><NavLink to="/become-guide" activeClassName='active' className="nav-link"> Become a Guide</NavLink></li>
</ul>
</Navbar>
)
}
//Admin main screen
const Admin = () => {
return (
<BrowserRouter>
<div className="wrapper">
<Navigation />
<div id="content" className="m-4">
<Switch>
<Route exact path="/dashboard">
<Dashboard />
</Route>
<Route exact path="/become-guide">
<BecomeGuide />
</Route>
</Switch>
</div>
</div>
</BrowserRouter>
)
}
export default Admin;
And here is the becomeguide.js:
//Guide module -navigation
const NavGuide =()=>{
return(
<Navbar>
<Form className="container-fluid justify-content-start">
<NavLink to="/become-guide/see-all" activeClassName='active'>See All</NavLink>
<NavLink to="/become-guide/approved" activeClassName='active' >Approved</NavLink>
<NavLink to="/become-guide/pending" activeClassName='active' >Pending</NavLink>
<NavLink to="/become-guide/rejected" activeClassName='active' >Rejected</NavLink>
</Form>
</Navbar>
)
}
const GuideSeeAll = ()=> {
return (
<div className="bg-dark">Hello See all</div>
);
}
const GuideApproved = ()=> {
return (
<div>Hello Approved</div>
);
}
const GuidePending = ()=> {
return (
<div>Hello Pending</div>
);
}
const GuideRejected = ()=> {
return (
<div>Hello Rejected</div>
);
}
const BecomeGuide = () =>{
return (
<Container>
<Row>
<NavGuide />
</Row>
<Row>
<BrowserRouter>
<Switch>
<Route exact path="/become-guide/all">
<GuideSeeAll />
</Route>
<Route exact path="/become-guide/approved">
<GuideApproved />
</Route>
<Route exact path="/become-guide/pending">
<GuidePending />
</Route>
<Route exact path="/become-guide/rejected">
<GuideRejected />
</Route>
</Switch>
</BrowserRouter>
</Row>
Sample Image:
Sample Image after buttons is clicked:
My problem is that, this becomeguide.js is from admin.js and becomeguide.js has it own navigation too, from the when I click the navigation from becomeguide.js nothing happens, its will no go to expected view. how can i fix this issue? Thank you!
Issue
Using the exact prop on the outer base route will necessarily preclude it from matching any sub routes. In other words, since the url path no longer exactly matches "/become-guide" when attempting to render a sub-route, i.e. like "/become-guide/see-all", that BecomeGuide is unmounted, and thus doesn't/can't render the nested routes.
There should be only one router rendering/providing a routing context to the app, so the nested BrowserRouter in BecomeGuide is unnecessary and is actually blocking the outer router from properly handling the nested routes.
Solution
Admin - Remove the exact prop on any routes rendering subroutes.
const Admin = () => {
return (
<BrowserRouter>
<div className="wrapper">
<Navigation />
<div id="content" className="m-4">
<Switch>
<Route path="/dashboard">
<Dashboard />
</Route>
<Route path="/become-guide">
<BecomeGuide />
</Route>
</Switch>
</div>
</div>
</BrowserRouter>
)
}
BecomeGuide - Remove the nested router. You can also probably remove the exact prop on these routes as well since they all have the same path specificity for matching.
<Row>
<Switch>
<Route path="/become-guide/all">
<GuideSeeAll />
</Route>
<Route path="/become-guide/approved">
<GuideApproved />
</Route>
<Route path="/become-guide/pending">
<GuidePending />
</Route>
<Route path="/become-guide/rejected">
<GuideRejected />
</Route>
</Switch>
</Row>

React router routes to wrong component with params

I am trying to use react-router to route toa. user profile page.
My User row component is as follows :
render() {
return (
<div className="userrow">
<div className="userrow-username">
{this.props.name}
</div>
<div>
<Link to={`/profile/${this.props.username}`} style={{ textDecoration: 'none' }}>
<Button variant="outlined" color="primary" size="small">
View Profile
</Button>
</Link>
</div>
</div>
)
}
The router:
public render() {
return (this.state.loading === true) ? <h1>Loading</h1> : (
<BrowserRouter>
<div>
<HeaderNav />
<Switch>
<Route exact={true} path={routes.LANDING} component={LandingPage} />
<Route exact={true} path={routes.SIGN_UP} component={SignUp} />
<Route exact={true} path={routes.LOGIN} component={Login} />
<Route exact={true} path={routes.PASSWORD_FORGET} component={PasswordForget} />
<Route path={routes.USER} component={UserList} />
<Route path={routes.PROJECT} component={Project} projectName='Nakul' />
<Route path={routes.HOME} component={Home} />
<Route exact={true} path="/profile/:username" component={UserProfile} />
</Switch>
</div>
</BrowserRouter>
);
}
I also have some protected routes which I have added as consumers to firebase auth. I can share the code if necessary, but I don't see that as the issue here.
The problem is, when I route to http://localhost:3000/profile/hellonakul I get redirected to home which is / path .
Instead I should be routed to my UserProfile component, which isn't happening.
EDIT:
routes:
export const SIGN_UP = "/signup";
export const LOGIN = "/login";
export const LANDING = "/landing";
export const HOME = "/";
export const ACCOUNT = "/account";
export const PASSWORD_FORGET = "/password_forget";
export const PROJECT = "/project";
export const USER = "/user";
I suppose that the Home route must be exact={true} too.
Edit: I think in your case, only the Home route needs an exact={true} attribute. This attribute is used to solve conflicts between similar routes.

react-router is not letting Link send a GET request to the server

Normally I would send a GET request using axios, but in the 'React Practice Course' tutorial, the instructor successfully hits this route
app.get('/api/admin/download/:filename', auth, admin, (req, res) => {
console.log('here I am ', req.params.filename);
const file = path.resolve('.')+`/uploads/${req.params.filename}`;
res.download(file);
})
with the Links in this code
import {Link} from 'react-router-dom';
showFiles = () => (
this.state.files &&
this.state.files.map((filename,i) => (
<li key={i}>
<Link to={`/api/admin/download/${filename}`}
target="_blank">
{filename}
</Link>
</li>
))
)
As seen here, the link matches the path in the server, but react router is sending me to the PageNotFound component instead, and never sends the GET request.
What am I missing to make a Link file reach the server? I suspect the problem is in my routes file.
const Routes = () => {
// null = public route
return (
<Layout>
<Switch>
<Route path="/admin/add_product" exact component={Auth(AddProduct, true)} />
<Route path="/admin/manage_categories" exact component={Auth(ManageCategories, true)} />
<Route path="/admin/manage_site" exact component={Auth(ManageSite, true)} />
<Route path="/admin/add_file" exact component={Auth(AddFile, true)} />
<Route path="/user/cart" exact component={Auth(Cart, true)} />
<Route path="/user/dashboard" exact component={Auth(Dashboard, true)} />
<Route path="/user/user_profile" exact component={Auth(UpdateProfile, true)} />
<Route path="/register_login" exact component={Auth(RegisterLogin, false)} />
<Route path="/register" exact component={Auth(Register, false)} />
<Route path="/product/:id" exact component={Auth(Product, null)} />
<Route path="/shop" exact component={Auth(Shop, null)} />
<Route path="/" exact component={Auth(Home, null)} />
<Route component={Auth(PageNotFound)} />
</Switch>
</Layout>
)
}
To expand my comments I answer here.
The following works for me:
import React from "react";
import { render } from "react-dom";
import { BrowserRouter as Router, Route, Link } from "react-router-dom";
const About = () => <div>About</div>
const Home = () => <div>Home</div>
const Topics = () => <div>Topics</div>
const apiUrl = "http://localhost:9000/api";
const BasicExample = () => (
<Router>
<div>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/about">About</Link>
</li>
<li>
<Link to="/topics">Topics</Link>
</li>
<li>
<a href={`${apiUrl}/v1/test`}>Api GET request</a>
</li>
</ul>
<hr />
<Route exact path="/" component={Home} />
<Route path="/about" component={About} />
<Route path="/topics" component={Topics} />
</div>
</Router>
);
render(<BasicExample />, document.getElementById("root"));
So basically I just created an <a> passing the proper href url and it didn't fall into <Router>, as indicated here: https://github.com/ReactTraining/react-router/issues/1147#issuecomment-113180174
Are you sure you are passing the right url to the anchor's href?

Nested route with a fixed component

In the below routing:
const Main = props => (
<main className=''>
<Account {...props.account}></Account>
<Route path="/tab1" component={Tab1}/>
<Route path="/tab2" component={Tab2}/>
</main>
);
const App = (props) => {
const state = props.store.getState();
return (
<Router history={browserHistory}>
<section >
<Route path="/main" render={() => (<Main account={state.account} />)} />
<Route path="/login" component={Login} />
<Link to='/login'>Login</Link>
<Link to='/main/tab1'>Tab1</Link>
<Link to='/main/tab2'>Tab2</Link>
</section>
</Router>
);
};
I want to have the Account component above other components (tabs). Tabs will load based on their route but Account is always there, except in /login page.
But what I get is:
In /login I get the Login as expected.
In /main/tab1 and /main/tab2 I only get Account and tab components don't render.
May questions are:
What I'm doing wrong?
Is there a way I can write this without defining Main component?
Thank you
Your routes in the component point to /tab1 and /tab2 but your url is /main/tab1 or /main/tab2. You have to prefix them with /main. This doesn't happen automatically. One way would be to pass your match object to your Main component and prefix your path with a template string, this is better than hardcoding it in case you change your url to the Main component:
const Main = props => (
<main className=''>
<Account {...props.account}></Account>
<Route path={`${props.match.url}/tab1`} component={Tab1}/>
<Route path={`${props.match.url}/tab2`} component={Tab2}/>
</main>
);
const App = (props) => {
const state = props.store.getState();
return (
<Router history={browserHistory}>
<section >
<Route path="/main" render={({match}) => (<Main match={match} account={state.account} />)} />
<Route path="/login" component={Login} />
<Link to='/login'>Login</Link>
<Link to='/main/tab1'>Tab1</Link>
<Link to='/main/tab2'>Tab2</Link>
</section>
</Router>
);
};

Different Layout - React Router v4

I want to have one layout for a set of paths, and another layout for another set of paths. Here is my route.jsx:
import React from 'react';
import { Route, Switch } from 'react-router-dom';
import Main from './components/main';
import Home from './components/home';
import MyWork from './components/work/myWork';
import WorkShow from './components/work/workShow';
const DefaultLayout = ({ children }) => (
<div>
<Main children={children} />
</div>
);
const Work = () => (
<Switch>
<Route exact path="/my-work" component={MyWork} />
<Route path="/my-work/:workName" component={WorkShow} />
</Switch>
);
const routes = (
<DefaultLayout>
<Switch>
<Route exact path="/" component={Home} />
<Route path="/my-work" component={Work} />
</Switch>
</DefaultLayout>
);
export default routes;
How would I add another router with a layout of BlogLayout, for example:
<BlogLayout>
<Switch>
<Route path="/blog" component={Blog} />
</Switch>
</BlogLayout>
Assuming you want the /blog path to be included in the switch, you can use the render function of the <Route> component:
const {
HashRouter,
Route,
Switch,
Link,
Redirect
} = ReactRouterDOM
const DefaultLayout = ({ children }) => (
<div>
<p>Main layout</p>
{children}
</div>
);
const AltLayout = ({ children }) => (
<div>
<p>Alternate layout</p>
{children}
</div>
);
const Home = () => (
<span>Home</span>
);
const Work = () => (
<span>Work</span>
);
const Blog = () => (
<span>Blog</span>
);
ReactDOM.render((
<HashRouter>
<div>
<ul>
<li><Link to="/">Home</Link></li>
<li><Link to="/work">Work</Link></li>
<li><Link to="/blog">Blog</Link></li>
</ul>
<p>The rendered route component:{' '}
<Switch>
<Route exact path="/" render={() => <DefaultLayout><Home /></DefaultLayout>} />
<Route path="/work" render={() => <DefaultLayout><Work /></DefaultLayout>} />
<Route path="/blog" render={() => <AltLayout><Blog /></AltLayout>} />
</Switch>
</p>
</div>
</HashRouter>
), document.getElementById('app'))
Note, for the working example on Codepen, I had to use <HashRouter>, but this should work regardless of the router you choose.

Resources