I'm having a strange issue with React Router. My PrivateRoute wrapper doesn't do its thing when navigating using a Link or NavLink.
Essentially I have something like this:
<BrowserRouter>
<Switch>
<Route exact path="/"><Home /></Route>
<PrivateRoute exact path="/private"><Private /></PrivateRoute>
</Switch>
</BrowserRouter>
PrivateRoute is just a wrapper around Route that checks for authentication.
Now, if I go to /private by typing it in the address bar, PrivateRoute does its job (and redirects to /login, but that doesn't matter).
However, if I use a NavLink in the Home component which has to="/private", React Router routes to the private route even if the user is unauthenticated.
Is there any way I can resolve this reasonably? And why does React Router behave like this and doesn't "go through" all the routes in BrowserRouter each time you navigate using a NavLink?
To solve your problem, you need to add exact or exact="true" on public Route.
If you don't include exact="true", <Route path="/"> refers all sub routes starting with /.
So /private will match / route hence it will render <Home /> component.
Here is updated code
<BrowserRouter>
<Switch>
<Route path="/" exact>
<Home />
</Route>
<PrivateRoute path="/private">
<Private />
</PrivateRoute>
</Switch>
</BrowserRouter>
Here is sample code
Well, this was a quite simple and dumb fix actually.
The problem was that the PrivateRoute component didn't "remount" when changing routes using Links (that is interesting and somehow clever behavior of React Router, I can imagine this improves performance).
Meanwhile, typing a URL into the address bar refreshes the entire page, thus "remounting" all components.
Since the logic for checking auth was in componentDidMount only, it didn't launch if the component wasn't "remounted", therefore creating my issue.
For any possible future visitors, to fix this problem, just move your auth checking logic from componentDidMount somewhere else, and reference it both in componentDidMount and componentDidUpdate. Don't forget to check if props actually changed in componentDidUpdate — otherwise you'll create an infinite loop — like so:
componentDidUpdate(prevProps, prevState) {
// You could also use shallowCompare, but that wasn't needed in my case
if (this.props.requiredPerms !== prevProps.requiredPerms) {
this.checkAuth(); // method with the logic which was in componentDidMount before
}
}
Related
I'm using React Router v5 and I ran into problems when trying to define routes split across multiple components. The case I tried is something like the following:
<Switch>
<MyRoutes />
<Route path="/foo">
<Foo />
</Route>
</Switch>
with the MyRoutes component looking like
<>
<Route path="/bar">
<Bar />
</Route>
<Route path="/baz">
<Baz />
</Route>
</>
The problem now is that routes declared after the custom MyRoutes component don't work, the custom component seems to match the route without rendering anything. The routes inside it do actually work as expected.
The reason I'm splitting the routes like this, apart from organizing related routes together is that I also need the separate routing component as a standalone to be able to integrate a set of components into a legacy web application. And I'd like to avoid duplicating the routes here for that purpose.
But obviously I'm doing something that you're not supposed to do with React Router, though I'm not entirely sure why this is an issue. So I would like to understand the limitation here a bit more to avoid running into variations of this again in the future. Why can't I split Routes like this into separate components within a single Switch, what are the exact limitations here?
Is putting a separate Switch into each component the right answer here? This does seem wrong from a conceptual point of view as each Route is supposed to be exclusive, only one of them should ever be rendered. Multiple Switch components would allow multiple components to be rendered at the same time, even though in practice this should not happen as the routes should be exclusive and not overlap.
What is the proper, intended way to have modular Routes in React Router v5 and not a single huge Routing component?
Issue
Routers inclusively render all matching routes whereas the Switch component exclusively renders the first match it finds. It's not strictly enforced but the only valid children of the Switch component are the Route and Redirect components. If you render any other component inside the Switch it will be returned and rendered and route matching completes, any routes after won't be reachable.
Switch
Renders the first child <Route> or <Redirect> that matches the
location.
How is this different than just using a bunch of <Route>s?
<Switch> is unique in that it renders a route exclusively. In
contrast, every <Route> that matches the location renders
inclusively.
Solution
Render MyRoutes in a route on its own so the Switch component can manage path matching and rendering. I suggest using path="/" so the nested routes can also further be matched.
<Switch>
<Route path="/foo">
<Foo />
</Route>
<Route
path="/" // <-- allows matching "/bar" and "/baz"
component={MyRoutes}
/>
</Switch>
MyRoutes
You will also want to render these routes into a Switch component. Any time you want to exclusively match routes you'll use the Switch component.
<Switch>
<Route path="/bar">
<Bar />
</Route>
<Route path="/baz">
<Baz />
</Route>
</Switch>
Also keep in mind that within the Switch component that path order and specificity matters. You will want to order the routes in inverse order of path specificity. This is to attempt to match and render more specific paths first, then falling back to less specific paths.
Example:
<Switch>
<Route path="/segment1/segment2/page" component={....} />
<Route path="/segment1/page" component={....} />
<Route path="/segment1" component={....} />
</Switch>
There may be a better solution here than using regex, but not sure how to go about it.
The issue is that I want my navigation component that lives in a component called MainLayout to show on all routes except on /login & `/logout
Currently, I have the following 3 routes
<Route exact path="/logout" component={Logout} />
<Route exact path="/login" component={Login} />
<Route
exact
path="/:subdirectory?/:nested?/:id?/"
component={MainLayout}
/>
So navigating to /login or /logout will render either Login or Logout and also MainLayout which is understandable with the current setup.
However, is there a way to use regex (or any other means) to check that if /:subdirectory is either /login or /logout then do not render component?
I'm trying to create an independent Route (not sure if that's the correct term) at BulletinBoard.js where I can use Link to go to Create Bulletin component.
I'm also trying to create another independent Route at BulletinList.js where I can navigate to Edit Bulletin with their respective IDs.
Before this, I tried using useRouteMatch with path and url, but apparently that wasn't the correct way to do it, now I'm told to use useLocation, it does adds the /createbulletin and /editbulletin/id paths behind the current URL, but it doesn't navigate to the component itself.
I've been cracking my head over this for the past 2 days and I still haven't figured out the correct way to do this.
Here is the codesandbox that I've created for reference.
The reason your code didnt navigate to a different component after the url changed is because you didnt use the exact attribute when declaring the route. So its matching /bulletinboard/anything and then it always renders de BulletinBoard component.
You could define all routes at the App.js file like
<Switch>
<Route path="/" component={Home} exact />
<Route path="/bulletinboard" component={BulletinBoard} exact />
<Route path="/bulletinboard/edit/:id" component={EditBulletinBoard} exact />
<Route path="/infohub" component={InfoHub} exact />
<Route component={NotFound} />
</Switch>
Also, check out the useHistory hook
So at the BulletinBoard.js when the user clicks the link
onClick={() => history.push(`/bulletinboard/edit/${id}`)}
Note that the edit route renders a different component that your codesandbox didn't have yet
I have this route that I want to have some stuff executing when the user leaves it.
So I found the onLeave hook
<Switch>
<Route exact path="/layout" onLeave={ console.log("LEFT LAYOUT") } render={(props) => <GridLayoutApps retracted={this.state.sideBarRetracted} {...props}/>} />
</Switch>
Its supposed to console.log "Left layout" whenever I'm in /layout, and decide to load another route. Point is, this doesn't seem to happen, since the console.log is loading each time I switch ANY route.
I have the routes in my app.js file, which I
export default withRouter(App);
I have encapsulated it like this in index
ReactDOM.render(<BrowserRouter><Provider store={store}><App /></Provider></BrowserRouter>, document.getElementById('root'));
onLeave needs to be a function. At present its just an normal prop
<Switch>
<Route exact path="/layout" onLeave={() => {console.log("LEFT LAYOUT")} } render={(props) => <GridLayoutApps retracted={this.state.sideBarRetracted} {...props}/>} />
</Switch>
However please note that onLeave is not longer available in react-router v4 onwards and you need to use the componentWillUnmount or the useEffect hook to trigger the same behaviour
I just took over a React project from one of my colleague but I cannot understand the logic in the code below.
content = <Switch>
<Route path="/login" exact component={LoginPage} />
<Route render={() => { return <Redirect to="/login" />; }} />
</Switch>
I know how to use Route with Component, with Render, but Render with Redirect, first time I saw it.
Thanks
This appears to be just another way of just saying:
<Redirect path='*' to='/login' />
Since it is inside a <Switch>, and after any <Route>, it will always match (if nothing above it got matched) and get rendered.
When the Redirect component gets rendered, it does its job of redirecting to the page specified in the to prop.
I found that out by doing some reading of the source code. If you're interested, there's a bit of indirection, but basically the Redirect component renders a Lifecycle component which will call method with the location provided as soon as it's mounted.
method is set like this:
const method = push ? history.push : history.replace;
And that's done like that because apparently the <Redirect> component can take push as a boolean prop, to set the behaviour of how the redirect is actually achieved.
Redirect component source https://github.com/ReactTraining/react-router/blob/master/packages/react-router/modules/Redirect.js
Lifecycle component source: https://github.com/ReactTraining/react-router/blob/master/packages/react-router/modules/Lifecycle.js