<Switch> component matching null value in react-router-4 - javascript

I'm trying to migrate to use React Router 4 and having some trouble understanding the logic of the <Switch> component as it's used in the docs to handle a 404 (or unmatched) route.
For my entry JavaScript file, I have the following routes set up.
index.js
<Switch>
<Route path="/login" component={Login} />
<Route path="/forgot-password" component={ForgotPassword} />
<Route path="/email-verification" component={EmailVerification} />
<Route component={App} />
</Switch>
The Login component will check to see if the user is authenticated, and if so, redirect the user to the /dashboard route (via history.replace).
The App component is only accessible when the user is authenticated and it has a similar check to redirect the user to /login if she is not.
In my App component I have more specified routes that I can be sure are only accessible if the user is logged in.
App.js
<Switch>
<Route path="/dashboard" component={Dashboard} />
<Route path="/accounts" component={Account} />
<Authorize permissions={['view-admin']}>
<Route path="/admin" component={Admin} />
</Authorize>
<Route path="/users" component={Users} />
<Route component={NotFound} />
</Switch>
Herein lies my problem. The Authorize component checks against the permissions passed to see if the user has those permissions, if so, it renders the children directly, if not, it returns null from render().
The expected behavior here is that the <Route path="/admin" /> does not render at all when there are insufficient permissions and the <Route component={NotFound} /> component renders.
According to the docs:
A renders the first child that matches. A
with no path always matches.
However, if I go to any route declared after the <Authorize> component, the router is matching to null. This means that, based on the example above, going to /users returns null. Is the expected behavior of react-router to return the first match in a <Switch/> component, even if it's a null value?
How can I provide a "catch-all" route (404) for such a situation without creating a <PrivateRoute> component for each of the many, authenticated routes in App.js? Should a null value really produce a match?

Unfortunately, react-router's Switch component won't work with routes nested inside other components like in your example. If you check the docs for Switch, it says:
All children of a <Switch> should be <Route> or <Redirect> elements.
... so your Authorize component is not actually legal there as a direct child of Switch.
If you have a read through the source code of the Switch component, you'll see that it rather evilly reads the props of each of its children and manually applies react-router's matchPath method on each child's path (or from) prop to determine which one should be rendered.
So, what's happening in your case is Switch iterates through its children until it gets to your Authorize component. It then looks at that component's props, finding neither a path or from prop, and calls matchPath on an undefined path. As you note yourself, "a <Route> with no path always matches", so matchPath returns true, and Switch renders your Authorize component (ignoring any subsequent Routes or Redirects, since it believes it found a match). The nested '/admin' route inside your Authorize component doesn't match the current path however, so you get a null result back from the render.
I'm facing a similar situation at work. My plan to work around it is to replace react-router's Switch in my routing code with a custom component which iterates through its children, manually rendering each one in turn, and returning the result of the first one that returns something other than null. I'll update this answer when I've given it a shot.
Edit: Well, that didn't work. I couldn't work out a supported way to manually invoke "render" on the children. Sorry I couldn't give you a workaround to Switch's limitations.

In case anyone reads this in >= 2019, one way to deal with this behaviour is to simply wrap the Route-component like so:
import React from 'react'
import { Route } from 'react-router-dom'
type Props = {
permissions: string[]
componentWhenNotAuthorized?: React.ElementType
}
const AuthorizedRoute: React.FunctionComponent<Props> = ({
permissions,
componentWhenNotAuthorized: ComponentWhenNotAuthorized,
...rest
}) => {
const isAuthorized = someFancyAuthorizationLogic(permissions)
return isAuthorized
? <Route {...rest} />
: ComponentWhenNotAuthorized
? <ComponentWhenNotAuthorized {...rest} />
: null
}
export default AuthorizedRoute
Then, simply use it as such:
import React from 'react'
import { Route, Switch } from 'react-router-dom'
import AuthorizedRoute from 'some/path/AuthorizedRoute'
import Account from 'some/path/Account'
import Admin from 'some/path/Admin'
import Dashboard from 'some/path/Dashboard'
import NotFound from 'some/path/NotFound'
import Users from 'some/path/Users'
const AppRouter: React.FunctionComponent = () => (
<Switch>
<Route
component={Account}
path='/accounts'
/>
<AuthorizedRoute
component={Admin}
componentWhenNotAuthorized={NotFound}
path='/admin'
permissions={['view-admin']}
/>
<Route
component={Dashboard}
path='/dashboard'
/>
<Route
component={Users}
path='/users'
/>
<Route
component={NotFound}
/>
</Switch>
)
export default AppRouter

Similar idea to what Robert said, here's how I did it
class NullComponent extends React.Component {
shouldComponentBeRenderedByRoute() {
return false;
}
render() {
return null;
}
}
class CustomSwitch extends React.Component {
render() {
return (
// React.Children.map returns components even for null, which
const children = React.Children.toArray(this.props.children).map(child => {
const { render, shouldComponentBeRenderedByRoute } = child.type.prototype;
if (shouldComponentBeRenderedByRoute && !shouldComponentBeRenderedByRoute.call(child)) {
return null;
}
if (shouldComponentBeRenderedByRoute) {
return render.call(child);
}
return child;
});
return <Switch>{children}</Switch>;
);
}
}
then use it just do
<CustomSwitch>
<Route path... />
<NullComponent />
<Route path... />
</CustomSwitch>
here, a component without shouldComponentBeRenderedByRoute function is assumed to be a valid Route component from react-router, but you can add more condition (maybe use path props) to check if it's a valid Route

Related

Add a component as props inside a Route with react-router-dom

Having the following code:
import { Switch, Route, BrowserRouter as Router } from 'react-router-dom';
export function MyComponent({ list }: MyComponentProps) {
return (
<Router>
<Switch>
<Route path={list[0].url} component={<NextComponent />} />
</Switch>
</Router>
);
}
I want to load a different component on that route.
Version of react-router-dom is 5.2.0.
This code is not working, it appears a red line under <Route path... stating this:
Operator '<' cannot be applied to types 'number' and 'typeof Route'.ts(2365)
(alias) class Route<T extends {} = {}, Path extends string = string>
import Route
Any ideas?
LATER EDIT:
There is an answer stating that replacing that line with component={NextComponent} will get rid of the error message. That's true but is it possible to send props to that component in this case?
For example, for component={<NextComponent someProps={something} />} seems possible but not for component={NextComponent}
In react-router-dom v5 you don't specify the component prop as a JSX literal (the RRDv6 syntax), just pass a reference to the component.
component={NextComponent} instead of component={<NextComponent />}
Code
export function MyComponent({ list }: MyComponentProps) {
return (
<Router>
<Switch>
<Route path={list[0].url} component={NextComponent} />
</Switch>
</Router>
);
}
If you need to pass along additional props then you must use the render prop. This effectively renders an anonymous React component. Don't forgot you may need to also pass along the route props to the rendered components.
export function MyComponent({ list }: MyComponentProps) {
return (
<Router>
<Switch>
<Route
path={list[0].url}
render={props => <NextComponent {...props} something={value} />}
/>
</Switch>
</Router>
);
}
Route render methods

How should I return HTML in a React.js class component without getting an error? [duplicate]

Matched leaf route at location "/" does not have an element. This means it will render an with a null value by default resulting in an "empty" page
//App.js File
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import Home from './pages/Home';
// import ReactDOM from "react-dom";
const App = () => {
return (
<Router >
<Routes>
<Route path="/" component={ Home }></Route>
</Routes>
</Router>
)
}
export default App;
**My any react router related code not working i don't know why it happend when i start insert some route in program so it show this error **
In V6, you can't use the component prop anymore. It was replaced in favor of element:
<Route path="/" element={<Home />}></Route>
More info in the migration doc.
I had the same problem. Replace component with element and it worked.
Replace this:
<Route path="/" component={HomePage} exact />
with this:
<Route path="/" element={<HomePage/>} exact />
I had the same error however my fix was slightly different
I had spelled element wrong.
<Route exact path='/MyGames' elemtent={<MyGames/>}/>
and this was the error it gave me in the browser console
Matched leaf route at location "/MyGames" does not have an element. This means it will render an <Outlet /> with a null value by default resulting in an "empty" page.
Very simple:
use element instead of component
wrap the your component like this: {<Home/>} instead of {Home}
<Route path="/" component={ <Home/> } />
in version 6:
component replaced with element and needs to close "</Route>"
<Route exact path="/" element={<AddTutorial />}></Route>
https://reactrouter.com/docs/en/v6/getting-started/overview
This is a common problem if you are using react-router-dom V6
To solve this it's simple
In your code
Replace component with element
Replace {home} with {}
This becomes...
<Route path="/" element={}>
This will definitely solve the problem.
If you're using react-router-dom 6 or above, you may have a routes array that includes parent and child routes. You may then try to open a route such as
/portal
and get this error because that component corresponds to a child route
/:customerid/portal
but you haven't read your routes (and their child routes) closely enough to see that.

Matched leaf route at location "/" does not have an element

Matched leaf route at location "/" does not have an element. This means it will render an with a null value by default resulting in an "empty" page
//App.js File
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import Home from './pages/Home';
// import ReactDOM from "react-dom";
const App = () => {
return (
<Router >
<Routes>
<Route path="/" component={ Home }></Route>
</Routes>
</Router>
)
}
export default App;
**My any react router related code not working i don't know why it happend when i start insert some route in program so it show this error **
In V6, you can't use the component prop anymore. It was replaced in favor of element:
<Route path="/" element={<Home />}></Route>
More info in the migration doc.
I had the same problem. Replace component with element and it worked.
Replace this:
<Route path="/" component={HomePage} exact />
with this:
<Route path="/" element={<HomePage/>} exact />
I had the same error however my fix was slightly different
I had spelled element wrong.
<Route exact path='/MyGames' elemtent={<MyGames/>}/>
and this was the error it gave me in the browser console
Matched leaf route at location "/MyGames" does not have an element. This means it will render an <Outlet /> with a null value by default resulting in an "empty" page.
Very simple:
use element instead of component
wrap the your component like this: {<Home/>} instead of {Home}
<Route path="/" component={ <Home/> } />
in version 6:
component replaced with element and needs to close "</Route>"
<Route exact path="/" element={<AddTutorial />}></Route>
https://reactrouter.com/docs/en/v6/getting-started/overview
This is a common problem if you are using react-router-dom V6
To solve this it's simple
In your code
Replace component with element
Replace {home} with {}
This becomes...
<Route path="/" element={}>
This will definitely solve the problem.
If you're using react-router-dom 6 or above, you may have a routes array that includes parent and child routes. You may then try to open a route such as
/portal
and get this error because that component corresponds to a child route
/:customerid/portal
but you haven't read your routes (and their child routes) closely enough to see that.

Private Route getting called dozens of times in React

I keep having maximum update depth exceeded errors and I can't figure out why.
I have the following (pared down, it was more complex originally and actually rendered the component) private route in a private route file:
class PrivateRoute extends Component {
render() {
console.log("private route");
return <Redirect to="/login" />;
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(withKeycloak(PrivateRoute));
And then in my index.js I have the following:
<Route
render={({ location }) => {
const { pathname } = location;
return (
<TransitionGroup>
<CSSTransition
key={pathname}
classNames="page"
timeout={{
enter: 1000,
exit: 1000
}}
>
<Route
location={location}
render={() => (
<Switch>
<Route path="/login" component={LoginPage} />
<Route path="/signup/" component={Signup} />
<PrivateRoute
exact
path="/cards/"
component={Wrapper}
/>
<PrivateRoute
exact
path="/"
component={Wrapper}
/>
...
This should, as far as I can tell, go to the PrivateRoute component for Wrapper on initial load, and then, redirect to the login page, which should not invoke the private route.
Instead, I see:
52 private route
in my console log.
Why am I being redirected back to PrivateRoute dozens of times? Shouldn't this happen once, and that's it?
There's no redirect to anywhere else on the login page at all. There is a login function, but that requires a button click, which is not happening.
Any idea on why this could be happening?
The error message:
in Lifecycle (created by Context.Consumer)
in Redirect (at PrivateRoute.js:11)
in PrivateRoute (created by Context.Consumer)
in WithKeycloak(PrivateRoute) (created by Context.Consumer)
in Connect(WithKeycloak(PrivateRoute)) (at src/index.js:114)
in Switch (at src/index.js:106)
in Route (at src/index.js:103)
in Transition (created by CSSTransition)
in CSSTransition (at src/index.js:95)
in div (created by TransitionGroup)
Originally, the routes looked more like this:
class PrivateRoute extends Component {
render() {
return (
<Route
{...rest}
render={props =>
<Component {...props} />}
/>
)
}
}
rather than
<PrivateRoute
component={Wrapper}
/>
You want to do something like
<Route exact path="/"
render=(props => (<PrivateRoute exact
path="/"
component={Wrapper}>)) />
Otherwise it will just always render your PrivateRoute
Route component is expected to receive a prop exact in order to only render this component when a match exists.
If not exact prop passed, will render it. And then if another match, with render both, and this is why you are getting redirected everytime.
Since you are using a custom component, you must handle this prop to provide it into the Route component.
to fix it, you can follow #TKol approach for example.
<Route exact path="/"
render=(props => (<PrivateRoute
path="/"
component={Wrapper}>))
/>
This way Route is handling that for you and only will render 1 at time.

How can I conditionally render particular html sections depending on the Route

I'm using React for this project.
I have got a Help section on my website, This help section has about 50+ articles. If you go to /help-open-account I want to render some html and if I go to /fraud I want to display different html it is all dynamic content so its using the same js file hence why I want to render depending on the link.
I know how to use the conditional statements but how can I use the current url as a statement so like path="/(?!help--lostMyCard)" if its currently on that url then render the content this content instead of x content.
The app.js file has this following code ->
<BrowserRouter>
<Switch>
<Route path="/" exact component={HelpSupport} />
<Route to="/help-articles" component={HelpArticles} />
</Switch>
</BrowserRouter>
Would the code above be the only way to conditionally render content? As this is rendering full components and I wanted to conditionally render subcomponents within them.
Just to clarify, I know how to use the routes and so on, but thats not the question, the question is when you are in a component for example you are in the Help components, can I conditionally render content depending on my Route and if so how?
So the HelpfulArticles I want to render that if my route is X but if my route is Y then render this other component?
I see that you want to access route information in component.
It seems that what you are looking for has already been solved here
How to get current route in react-router 2.0.0-rc5
For more information -
Someone can club understanding with link here https://reactjs.org/docs/conditional-rendering.html in addition to above stackoverflow link i have mentioned in this comment to completely solve such problem.
if I understood correctly you want to add more routes based on your path:
Here below let's say help-articles has subpages that might have different HTML to render.
<BrowserRouter>
<Switch>
<Route path="/" exact component={HelpSupport} />
<Route path="/help-articles/:subpage?" component={HelpArticles} />
</Switch>
SO first to add subpage optional parameter.
Then in your HelpArticles component render method:
return {
const {match} = this.props;
const {params} = match;
if(!params.subpage) {
return <div> no subpage routes</div>
}else if(params.subpage === "fruits") {
return <FruitsHelper>
} else if(params.subpage === "veg") {
return <VegsHelper>
}
}
update the code here,
<BrowserRouter>
<Switch>
<Route path="/" exact component={HelpSupport} />
// not `to`, it is `path`
<Route path="/help-articles" component={HelpArticles} />
</Switch>
</BrowserRouter>
now, you can update HelpArticles component like below,
import React, { Component } from "react";
import List, { LostMyCard, LockedAccount } from "../custom/help";
class HelpArticles extends Component {
_renderArticle() {
const { location: { pathname } } = this.props;
// render the components by comparing the url, here you can use switch too.
if ((pathname || "").includes("lostMyCard")) {
return <LostMyCard />
}
if ((pathname || "").includes("locked-account")) {
return <LockedAccount />
}
// if no case is match then default component for listing the articles.
return <List />
}
render() {
return (
<React.Fragment>
{this._renderArticle()}
</React.Fragment>
)
}
}
export default HelpArticles;

Categories