I have created a HOC "ProtectedRoute" to restrict paths for unauthenticated users. I have used "react-router-dom" for routing in the application.
I am able to route the users based on their token but I am receiving a warning in the console and I am unable to access routerprops such as "history", "location", "match" in rendered component. Below is the ProtectedRoute component implementation.
const ProtectedRoute = ({component:Component, ...rest}) => {
const [isLoggedIn, setLoggedIn] = useState({loggedIn:false, loaded:false})
useEffect(async () => {
const userStatus = await validateUserLoggedIn(); # returns true if user is authenticated
setLoggedIn((prevState) => {return {loggedIn:userStatus, loaded:true}})
}, [])
return(
<Route {...rest} render={(routerProps) => {
return isLoggedIn.loaded ?
isLoggedIn.loggedIn ? <Component {...routerProps} {...rest} /> : <Redirect to={{pathname:'/login'}} :
<h1>Loading Page</h1>
}} />
)
}
ProtectedRoute in main routing component:
<Switch>
<ProtectedRoute exact path="/admin" component={Admin} />
</Switch>
Error Message "Functions are not valid as a React child. This may happen if you return a Component instead of from render. Or maybe you meant to call this function rather than return it".
Any suggestion is appreciated.
Ok so there a few things wrong.
You should not return something inside a component, you are doing this inside your <Route /> component. This is causing the warning: Functions are not valid as a React child. This may happen if you return a Component instead of from render.
Your <Redirect /> component was missing a /> at the end of it, leaving it open will cause an error.
These fixes should also solve your problem where you couldn't access the route props.
<Route
{...rest}
render={(routerProps) => {
isLoggedIn.loaded ? (
isLoggedIn.loggedIn ? (
<Component {...routerProps} {...rest} />
) : (
<Redirect to={{ pathname: '/login' }} />
)
) : (
<h1>Loading Page</h1>
);
}}
/>;
Related
I've got the following structure in my React app, using react-router-dom.
<Router>
<Header/>
<Main>
<AllRoutes> // THIS HANDLES THE SWITCH WITH ALL THE ROUTES
<Switch>
<Route exact path={ROUTES.HOME} component={Home}/>
<Route exact path={ROUTES.ABOUT} component={About}/>
<Route exact path={ROUTES.PRIVACY} component={Privacy}/>
// ETC
</Switch>
</AllRoutes>
</Main>
<Footer/> // <==== FOOTER NEEDS TO KNOW WHICH ROUTE HAS BEEN MATCH
<Router>
QUESTION
Footer needs to know what <Route/> has been match. What is the best pattern to achieve that?
OPTION #1
I found the useRouteMatch hook over on react router docs:
This would kind of work, but I don't think it is good enough for my situation. Because a URL string can match a route and still don't be a valid route at the same time.
For example:
Route: /:language/privacy
Valid route: /en/privacy
Not valid route that would also match: /notALanguage/privacy
Once a route has match, I usually need to check if it is valid before rendering a component page or the 404 page.
Like:
<Route exact path={"/:language/privacy"} render={(routeProps) => {
const possibleLanguage = routeProps.match.params.language;
if (possibleLanguage in LANGUAGES) {
return(
<PrivacyPage lang={possibleLanguage}/>
);
}
else {
return(
<Page404/>
);
}
}}/>
OPTION #2
What I'm thinking about doing is:
App.js calls useLocation. So it always re-render when there is a route change.
I could add a detectRoute function in App.js to do all the route checking beforehand.
And my AllRoutes component wouldn't need a component. I would implement a native JS switch and render the corresponding route.
This way I know upfront which <Route/> is going to match and I can pass it on to <Footer/> or any component that lives outside of the matched <Route/>.
Something like this:
SandBox Link
export default function App() {
console.log("Rendering App...");
const location = useLocation();
// THIS WOULD BE THE detectRoute FUNCTION
// I COULD EVEN USE THE useRouteMatch HOOK IN HERE
const matchedRoute =
location.pathname === ROUTE1
? "ROUTE1"
: location.pathname === ROUTE2
? "ROUTE2"
: "404";
return (
<div>
<div className="App">
<Link to={ROUTE1}>Route 1</Link>
<Link to={ROUTE2}>Route 2</Link>
<Link to={"/whatever"}>Route 404</Link>
</div>
<div>
<AllRoutes matchedRoute={matchedRoute} />
</div>
</div>
);
}
function AllRoutes(props) {
switch (props.matchedRoute) {
case "ROUTE1":
return <Route exact path={ROUTE1} component={Page1} />;
case "ROUTE2":
return <Route exact path={ROUTE2} component={Page2} />;
default:
return <Route exact path={"*"} component={Page404} />;
}
}
It works. But I would like to know if there's a proper way of doing this, 'cause this seems a bit weird and there might be something out there that was specifically designed for this.
Generally you want to either:
Wrap the components together
Create another switch to route them (and pass match params)
I put together a somewhat comprehensive example of the options. Hope that helps!
import React from "react";
import "./styles.css";
import { Switch, Link, Route, BrowserRouter } from "react-router-dom";
const hoc = (Component, value) => () => (
<>
<main>
<Component />
</main>
<Footer value={value} />
</>
);
const Wrapper = ({ component: Component, value }) => (
<>
<main>
<Component />
</main>
<Footer value={value} />
</>
);
const WrapperRoute = ({ component, value, ...other }) => (
<Route
{...other}
render={props => <Wrapper component={component} value={value} {...props} />}
/>
);
const Footer = ({ value }) => <footer>Footer! {value}</footer>;
const Header = () => <header>Header!</header>;
const Another = () => <Link to="/onemore">One More!</Link>;
const Home = () => <Link to="/other">Other!</Link>;
const OneMore = () => <Link to="/">Home!</Link>;
const Other = () => <Link to="/another">Another!</Link>;
export default () => (
<BrowserRouter>
<Header />
<Switch>
{/* You could inline it! */}
<Route
path="/another"
render={() => (
<>
<main>
<Another />
</main>
<Footer value="" />
</>
)}
/>
{/* You could use a custom route component (that uses an HOC or a wrapper) */}
<WrapperRoute
component={OneMore}
path="/onemore"
value="I got one more!"
/>
{/* You could use a Higher-Order Component! */}
<Route path="/other" component={hoc(Other, "I got other!")} />
{/* You could use a wrapper component! */}
<Route
path="/"
render={() => <Wrapper component={Home} value="I got home!" />}
/>
</Switch>
{/* You could have another switch for your footer (inline or within the component) */}
<Switch>
<Route
path="/another"
render={() => <Footer value="Switch footer another!" />}
/>
<Route
path="/other"
render={() => <Footer value="Switch footer other!" />}
/>
<Route
path="/onemore"
render={() => <Footer value="Switch footer onemore!" />}
/>
<Route path="/" render={() => <Footer value="Switch footer home!" />} />
</Switch>
</BrowserRouter>
);
Note the WrapperRoute would allow you to do validation on your match params before passing them through. You could do a Redirect if needed.
What I've ended up doing:
Since I'm using Redux, I added a piece of global state to keep track of the matched route.
And I dispatch actions to update that state from the render prop from the <Route/>'s component.
<Switch>
<Route key={index} exact path={"/some-route"} render={(routeProps) => {
// HERE I DISPATCH AN ACTION TO CHANGE THE STATE FOR THE CURRENT ROUTE
dispatch({
type: UPDATE_CURRENT_ROUTE,
payload: { name: "SOME_ROUTE_NAME" }
});
return (
<PrivacyPage
{...routeProps}
/>
);
}}/>
</Switch>
And now I can do on Footer.js:
function Footer() {
const currentRoute = useSelector((state) => state.currentRoute);
// RENDER FOOTER ACCORDINGLY TO THE CURRENT ROUTE
}
I am setting up react router for different link in my project but the problem is I need react router to tell the difference between a user username variable and other paths.
For example:
baseUrl/admin
baseUrl/daniel
React doesnt know the difference. I will have a list of usernames in a db and would return an error if the user doesnt exist then that means the page does not exist.
This is my code:
class App extends Component{
render(){
return (
<Router>
<Route exact path="/" render={props => (
<React.Fragment>
<h1>Hey</h1>
</React.Fragment>
)}
/>
<Route exact path="/admin" render={props => (
<React.Fragment>
<h1>admin</h1>
</React.Fragment>
)}
/>
<Route path="/:user" component={UserComponent}
/>
</Router>
);
}
}
You can use the match.url property to choose which component render, for example:
<Route path="/:user" render={props => {
if(props.match.url === '/admin') {
return (
<React.Fragment>
<h1>Hey</h1>
</React.Fragment>
)
} else return (<UserComponent {...props} />)
}} />
I am experiencing the following problem:
I have two screens in my application, one if the user has access and one if not.
If the user has access to the system, he will be redirected to the screen A, a private route that has internal states, when the private routes change, the internal state of that screen A should continue until he changes to a non-private or unknown route.
The point is, I have a private routes vector, but when I loop these routes and add a key to each Router component, on each change of route, it will unmount and mount component A (Code sample here), so I lose the internal state of A, and if I add the key to the child component of A, the internal state remains as I would like (Code sample here), however I break the child key rule of react.
Warning: Each child in a list rule should have a unique" key "prop.
Any help would be amazing! :)
#Edit: the code snippet of first sandbox. The difference between the first one and the second is the key prop, instead it be inside Route, it is within the component.
#Edit 2:
I've fixed it cdeclaring all routes statically and letting the access policy come dinamically. Ty for help!
If anyone find a better solution, It'll be wellcome! :)
{ canAccess: true, path: "/home", component: () => <div>Home</div> },
{ canAccess: true, path: "/foo", component: () => <div>Foo</div> },
{ canAccess: false, path: "/blah", component: () => <div>Blah</div> }
];
const Homepage = () => {
return (
<div>
<Link to="/home">Home</Link>
<br />
<Link to="/foo">Foo</Link>
<br />
<Link to="/blah">Blah</Link>
</div>
);
};
const Main = ({ children }) => {
const [innerState, setInnerState] = useState(112);
return (
<div>
{children}
{JSON.stringify(innerState)}
<br />
<button onClick={() => setInnerState(innerState + 1)}>AddNumber</button>
<Homepage />
</div>
);
};
const PrivateRoute = ({ component: Component, path, canAccess, index }) => (
<Route
key={index}
path={path}
render={() =>
canAccess ? (
<Main>
<Component />
</Main>
) : (
<div>Not found :(</div>
)
}
/>
);
function App() {
return (
<div className="App">
<BrowserRouter>
<Switch>
{defaultRoutes.map((route, index) => {
return PrivateRoute({ index, ...route });
})}
<Route path="/" exact component={() => <Homepage />} />
<Route component={() => <div>Not found :(</div>} />
</Switch>
</BrowserRouter>
</div>
);
}
I've fixed it cdeclaring all routes statically and letting the access policy come dinamically. Ty for help!
If anyone find a better solution, It'll be wellcome! :)
I am using react-router-dom 4.2. I have my App.js with Authenticated components inside. This components are created by me and add a little of business logic, create the component via React.createElement, and route them via Route component. Nothing unusual.
The App.js:
// ...
const App = props => (
<BrowserRouter>
<Fragment>
<Switch location={location}>
<Route
exact
path={URLS.ROOT}
render={() => <Redirect to={URLS.DASHBOARD} />}
/>
<Authenticated
{...props}
resource={ResourcesCode.DASHBOARD}
patent={PatentsCode.VIEW}
path={URLS.DASHBOARD}
component={Dashboard}
/>
<Authenticated
{...props}
resource={ResourcesCode.SUBSCRIBE}
patent={PatentsCode.VIEW}
path={URLS.SUBSCRIBE}
component={Subscribe}
/>
</Fragment>
</BrowserRouter>
// ...
Inside of the component Subscribe (mentioned above in the 2nd Authenticated component), I have more routes as you can see below:
// ...
<Route
path={URLS.SUBSCRIBE}
exact
render={() => (
//...
)}
/>
<Route
path={URLS.SUBSCRIBETWO}
exact
render={() => (
//...
)}
/>
// ...
The point is that this routes on the child component (Subscribe) are ignored.
Why are them ignored? How can I solve it?
I really need this routes inside the child component. I don't want to move them to App.js
IMPORTANT EDIT:
The second route is ignored, I realized that the first doesn't. In other words, The Route component with path={URLS.SUBSCRIBE} is working, but the component with path={URLS.SUBSCRIBETWO} is ignored, so here is the problem to solve.
EDIT:
For if you need, the Authenticated component:
// ...
}) => (
<Route
path={path}
exact={exact}
render={route => {
if (!authenticated) {
if (loggingIn) {
return '';
}
return <Redirect to={URLS.LOGIN} />;
}
if (!roleSubReady) {
return '';
}
if (path !== URLS.SUBSCRIBE && user.pendingSubscription) {
if (isLoading) {
return '';
}
return <Redirect to={URLS.SUBSCRIBE} />;
}
if (path === URLS.SUBSCRIBE && !user.pendingSubscription) {
if (isLoading) {
return '';
}
return <Redirect to={URLS.DASHBOARD} />;
}
if (resource && !checkPermission(user, resource, patent)) {
return <NotAuthorized history={route.history} />;
}
return (
<React.Fragment>
<Menu user={user} path={path} isLoading={isLoading} />
<Header show={showHeaderAndFooter} user={user} path={path} />
<MainContent>
{React.createElement(component, {
user,
resource,
...route,
})}
<Footer show={showHeaderAndFooter} />
</MainContent>
</React.Fragment>
);
}}/>
);
I got this warning
Warning: You should not use <Route component> and <Route render> in the same route; <Route render> will be ignored
Not sure it's causing the Redirect to fail, but my code below just won't work, it's fine if this.isAuth is true, but not with <Redirect />
https://codesandbox.io/s/5xm202p1j4
render() {
const { Component: component, ...rest } = this.props;
return (
<Route
{...rest}
render={props =>
this.isAuth === false ? (
<Component {...props} />
) : (
<Redirect
to={{
pathname: "/login"
}}
/>
)
}
/>
);
}
You have destructured your props incorrectly, it should be const { component: Component, ...rest } = this.props;
The reason that it was giving you the warning was because you tried to destructure Component with uppercase C from props whereas you pass component as props to the authRoute, and not since Component is now present in props the rest params contains the component props which is passed down to the Route.
render() {
const { component: Component, ...rest } = this.props;
return (
<Route
{...rest}
render={props =>
this.isAuth === false ? (
<Component {...props} />
) : (
<Redirect
to={{
pathname: "/login"
}}
/>
)
}
/>
);
}
Working sandbox