Conditional routing in React - reactjs

I want to only be able to load a component if the user is authenticated to. Normally I have this PrivateRoute component for that:
const PrivateRoute = ({ component: Component, hasAccess, addUser, ...rest }) => (
<Route
{...rest}
render={props =>
hasAccess === true ? <Component {...props} addUser={addUser} /> : <Redirect to="/" />
}
/>
);
Which I call like this:
<PrivateRoute
hasAccess={hasAccess}
path="/settings"
component={Setting}
/>
But in the other case I can't reuse this code. So I decided to just declare the route like this:
<Route
createMeeting={createMeeting}
path="/meetings"
component={MeetingRoutes}
render={props =>
createMeeting === true ? <Component {...props} createMeeting={createMeeting} /> : <Redirect to="/" />
} />
It should act the same as the PrivateRoute if you ask me, but it doesn't. Instead I got this error message:
Warning: You should not use <Route component> and <Route render> in
the same route; <Route render> will be ignored
Can someone explain to me why it gives me this error? I can't find the solution for my problem.

It will better to write a reusable PrivateRoute:
const PrivateRoute = ({ component: Component, hasAccess, componentProps = {}, redirectTo = "/", ...rest }) => (
<Route
{...rest}
render={props =>
hasAccess ? <Component {...props} {...componentProps} /> : <Redirect to={redirectTo} />
}
/>
);
So with Settings you could use this route like this:
<PrivateRoute
hasAccess={hasAccess}
path="/settings"
component={Setting}
componentProps={{
addUser: addUser
}}
/>
and with MeetingRoutes like this:
<PrivateRoute
hasAccess={hasAccessToMeetingRoutes}
path="/meetings"
component={MeetingRoutes}
componentProps={{
createMeeting: createMeeting
}}
/>

Wrap your route in ternary operator and add one Redirect component at the end of routes.

You can use HOC to wrap component of route need authenticated other components no need authenticated you can render them normally
Example HOC authentication:
const loginGuard = Component =>
connect(state => ({
auth: getAuth(state),
}))(({ auth, ...props }) => {
if (auth) {
return <Component auth={auth} {...props} />;
}
// push to sign in if try access to component requires authentication
history.push('/sign-in');
return null;
});
Usage:
<Route
{...rest}
component={loginGuard(AdminComponent)}
/>

Just remove the component property in your new Route, like this:
const DynamicComp = MeetingRoutes
return <Route
createMeeting={createMeeting}
path="/meetings"
render={props =>
createMeeting === true ? <DynamicComp {...props} createMeeting={createMeeting} /> : <Redirect to="/" />
} />

Related

Avoid error "Element type is invalid" with React.createElement in HOC React

I want to implement the PrivateRoute HOC which will check if the user is logged in then render the corresponding component, otherwise redirect to the login page.
Here is the PrivateRoute HOC:
import React from 'react';
import { Route, Redirect } from 'react-router-dom';
const PrivateRoute = ({ component, ...rest }) => (
<Route {...rest} render={(props) => (
sessionStorage.getItem('userToken') ? (
React.createElement(component, props)
) : (
<Redirect to={{
pathname: '/login',
}} />
)
)} />
);
export default PrivateRoute;
Here's how I'm using it:
import PrivateRoute from './components/HOC/PrivateRoute';
...
render() {
if (this.props.hasError) return <h1>Sorry, something went wrong...</h1>;
if (this.props.isLoading) return <h1>Loading...</h1>;
return (
<div className="App">
<Switch>
<PrivateRoute exact path="/" component={<Home /> } />
<Route exact path="/login" render={props => <Login {...props} />} />
<Route exact path="/registration" render={props => <Registration {...props} />} />
<PrivateRoute exact path="/account" component={<Account />} />
</Switch>
</div>
);
}
But it throws an error:
I find a GitHub thread where I find a possible answer for why it happens like that, but I don't know how to implement that answer for my case:
I tried some experimental ways to achieve that but everything fails.
React.createElement accepts a type or a React component before it invoked.
React.createElement(
type,
[props],
[...children]
)
The type argument can be either a tag name string (such as 'div' or 'span'), a React component type (a class or a function), or a React fragment type.
const Component = () => <>Hello</>;
// Good
React.createElement(Component)
React.createElement("div")
// Bad
React.createElement(<div/>)
In your case you use the "bad" version.
You should use cloneElement or fix it with a wrapper:
// Good
React.cloneElement(<Component />)
// Or make a wrapper
React.createElement(() => <Component />));
So finally:
// Change to clone
React.cloneElement(component, props)
// **OR** make a wrapper
<PrivateRoute exact path="/" component={() => <Home />} />

Create PrivateRoute with React-Router-Dom

I have seen lots of use cases about how people create private route with react-router-dom.
It usually looks like this:
const PrivateRoute = ({component: Component, auth:{ isAuthenticated }, ...rest}) => (
<Route {...rest} render={props => (
isAuthenticated ?
<Component {...props} />
: <Redirect to="/signin" />
)} />
);
And my question is: If I can use this way to do the same thing?
const PrivateRoute = ({component: Component, auth:{ isAuthenticated }, ...rest }) => (
isAuthenticated ? <Route {...rest} render={props => <Component {...props}/>}/> :<Redirect to="/signin" />
)
It seems working when I ran the code. But I still want to know why we do the condition check at the first way of writing? Is the second way of writing incorrect? If so, why? Thank you!
It's just a preference thing, either is fine and both will work. you can also make it even shorter and more readable like this if you like
const PrivateRoute = ({component: Component, auth:{ isAuthenticated }, ...rest }) => (
isAuthenticated ? <Component {...rest} /> : <Redirect to="/signin" />
)
and then render like so:
<PrivateRoute
exact
component={ComponentToRender}
path="/path"
aProp={'aProp'}
/>

Is it possible to match the # part for any route in React Router 5?

In my app, I'd like to match all routs that end with #something.
/map#login
/info#login
and
/map#register
/map/one#register
/info#register
/info/two#register
So I can show component as popup on top of the content. How this can be done?
I found a solution for this case. It was inspired from this question in stackOverflow. Using HashRoute wrapper for Route and showing component based on location.hash.
const HashRoute = ({ component: Component, hash, ...routeProps }) => (
<Route
{...routeProps}
component={({ location, ...props }) =>
location.hash === hash && <Component {...props} />
}
/>
);
export default class App extends Component {
constructor() {
super();
}
render() {
return (
<div className='App'>
<Router history={history}>
<HashRoute hash='#login'component={Login} />
<HashRoute hash='#register' component={Register} />
<Switch>
<Route exact path='/map' component={Map} />
<Route exact path='/info' component={Info} />
</Switch>
</Router>
</div>
);
}
}
Updating/improving from the other answer here. It would better to not use the component prop as it won't create new instance of the routed component each time the Route is rendered for any reason. The custom HashRoute component should return valid JSX, either a Route component or null.
Example:
const HashRoute = ({ hash, ...routeProps }) => {
const location = useLocation();
return location.hash === hash
? <Route {...routeProps} />
: null
};
...
<Router>
<HashRoute hash='#login' component={Login} />
<HashRoute
hash='#register'
render={props => <Register {...props} otherProp />}
/>
<HashRoute hash='#something'>
<Register otherProp />
</HashRoute>
<Switch>
<Route path='/map' component={Map} />
<Route path='/info' component={Info} />
</Switch>
</Router>

How to pass a value from mapStateToProps into a function?

I am trying to make the following link inaccessible if a user is not logged in:
<PrivateRoute path="/page" exact component={page}/>
React returns an error on this code:
const PrivateRoute = ({component: Component, ...rest}) => (
<Route {...rest} render={(props) => (
this.props.isAuthenticated === true
? <Component {...props} />
: <Redirect to='/login'/>
)}/>
);
And the is.Authenticated part
const mapStateToProps = state => {
return {
isAuthenticated: state.token !== null
};
};
TypeError: Cannot read property 'props' of undefined
I am trying to access the isAuthenticated passed from my mapStateToProps in the function PrivateRoute, how can I do that ?
First of all you should use rest instead of this.props (which is not defined)
rest.isAuthenticated === true
and not:
this.props.isAuthenticated === true
Then you have to pass the prop to the private route, something like this:
<PrivateRoute isAuthenticated={some_variable_here}> // According to your validation logic
Try this:
const PrivateRoute = ({component: Component, ...rest}) => (
<Route {...rest} render={() => (
!!rest.isAuthenticated === true
? <Component {...rest} />
: <Redirect to='/login'/>
)}/>
);
I have created my answer in combination of #Emanuele and #Sultan.
So First I changed my PrivateRoute from
<PrivateRoute path="/page" exact component={page}/>
to
<PrivateRoute path="/page" exact component={page} auth={this.props.isAuthenticated}/>
This way I can pass the value from mapStateToProps in the PrivateRoute
Then I edited the function PrivateRoute to get the value of auth from the PrivateRoute. It now looks like follows
const PrivateRoute = ({component: Component, ...rest}) => (
<Route {...rest} render={(props) => (
rest.auth !== false
? <Component {...props}/>
: <Redirect to='/'/>
)}/>
);
And now the privateRoute function is aware of the isAuthenticated value passed from the connect

How to add Typescript Interface for a ReactRouter's PrivateRoute?

Im using React Router v4 and has created a private route, along the following lines:
const PrivateRoute = ({ component: Component, ...rest }) => {
return (
<Route
{...rest}
render={(props) => {
return isUserLoggedIn() ? <Component {...props} /> : <Redirect to="/login" />;
}}
/>
);
};
And it is used like this:
<Switch>
<Route path="/login" component={Login} />
<PrivateRoute exact path="/" component={Home} />
<PrivateRoute path="/vehicles" component={Vehicles} />
</Switch>
Now, I get the following error:
Binding element 'Component' implicitly has an 'any' type.
This is because, I have given noImplicitAny AND I have not specified Interface type for this PrivateRoute props.
Question is, How do I add interface to the props here?
Note: It is especially the ...rest part in the props, that Im primarily confused at typescripting.
Try this:
export interface PrivateRouteProps extends RouteProps {
component: Component;
}
const PrivateRoute = (props: PrivateRouteProps) => {
let { component: Component, ...rest } = props;
return (
<Route
{...rest}
render={(props) => {
return isUserLoggedIn() ? <Component {...props} /> : <Redirect to="/login" />;
}}
/>
);
};

Resources