Reputation: 413
I am using react router v4 with thunk for routing in my application.
I want to prevent rendering <AccountPage />
component to user who not logged in. I sending fetch request on server with id and token to check in database do user has this token. If it has - render <AccountPage />
, if not - redirect home.
I don't understand what is good way to implement the "conditional routing", and i found something which seems almost perfectly fit to my task.
https://gist.github.com/kud/6b722de9238496663031dbacd0412e9d
But the problem is that condition
in <RouterIf />
is always undefined, because of fetch's asyncronosly. My attempts to deal with this asyncronously ended with nothing or errors:
Objects are not valid as a React child (found: [object Promise]) ...
or
RouteIf(...): Nothing was returned from render. ...
Here is the code:
//RootComponent
<BrowserRouter>
<Switch>
<Route exact path='/' component={HomePage}/>
<Route path='/terms' component={TermsAndConditionsPage}/>
<Route path='/transaction(\d{13}?)' component={TransactionPage}/>
<RouteIf
condition={( () => {
if( store.getState().userReducer.id, store.getState().userReducer.token) {
// Here i sending id and token on server
// to check in database do user with this id
// has this token
fetch(CHECK_TOKEN_API_URL, {
method: 'post',
headers: {'Accept': 'application/json', 'Content-Type': 'application/json'},
body: JSON.stringify({
id: store.getState().userReducer.id,
token: store.getState().userReducer.token
})
})
.then res => {
// If true – <RouteIf /> will render <AccountPage />,
// else - <Redirect to="/">
// But <RouteIf /> mounts without await of this return
// You can see RouteIf file below
if(res.ok) return true
else return false
})
}
})()}
privateRoute={true}
path="/account"
component={AccountPage}
/>
</Switch>
</BrowserRouter>
//RouteIf.js
const RouteIf = ({ condition, privateRoute, path, component }) => {
// The problem is that condition is
// always undefined, because of fetch's asyncronosly
// How to make it wait untill
// <RouteIf condition={...} /> return result?
return condition
? (<PrivateRoute path={path} component={component} />)
:(<Redirect to="/" />)
}
export default RouteIf
How to make condition
wait until fetch
return answer? Or maybe there is another, better way to check if user logged in?
Upvotes: 4
Views: 8742
Reputation: 1973
You can wrap your route in a stateful component.
Then, on componentDidMount
check the token and and set token in state.
Then in render conditionally mount the route on state property.
class CheckToken extends React.Component {
constructor() {
this.state = { isLogged: false}
}
componentDidMount() {
fetch(url).then(res => res.text()).then(token => this.setState({isLogged: token})
}
render() {
return this.state.isLogged ? <Route path='/terms' component={TermsAndConditionsPage}/> : null
}
}
Upvotes: -1
Reputation: 21
In my case the problem was that after each refresh of the private page system redirected user to home before auth checking was executed, by default token value in the store was null, so user was unauthorized by default. I fixed it by changing default redux state token value to undefined. "undefined" means in my case that the system didn't check yet if the user is authorized. if user authorized token value will be some string, if not authorized - null, so PrivateRoute component looks
import React from 'react';
import {Redirect, Route} from "react-router-dom";
import {connect} from "react-redux";
const PrivateRoute = ({children, token, ...props}) => {
const renderChildren = () => {
if (!!token) {// if it's a string - show children
return children;
} else if (token === undefined) { // if undefined show nothing, but not redirect
return null; // no need to show even loader, but if necessary, show it here
} else { // else if null show redirect
return (
<Redirect
to={{
pathname: "/",
}}
/>
);
}
};
return (
<Route {...props}>
{renderChildren()}
</Route>
)
};
function mapStateToProps(state) {
return {
token: state.auth.token,
}
}
export default connect(mapStateToProps)(PrivateRoute);
App.js
<Route path="/" exact component={Home}/>
<PrivateRoute path="/profile" component={Profile}/>
Upvotes: 2
Reputation: 76
If you are using redux you can show temporary 'loading ...' view. The route will be redirected only if a user is null and loaded.
PrivateRoute.js
import React from 'react';
import PropTypes from 'prop-types';
import { useSelector } from 'react-redux';
import { Route, Redirect } from 'react-router-dom';
import { selectors } from 'settings/reducer';
const PrivateRoute = ({ component: Component, ...rest }) => {
const user = useSelector(state => selectors.user(state));
const isLoaded = useSelector(state => selectors.isLoaded(state));
return (
<Route
{...rest}
render={props =>
!isLoaded ? (
<></>
) : user ? (
<Component {...props} />
) : (
<Redirect to='/sign_in' />
)
}
/>
);
};
export default PrivateRoute;
PrivateRoute.propTypes = {
component: PropTypes.any
};
routes.js
import React from 'react';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import PrivateRoute from './components/PrivateRoute';
export const Routes = () => (
<BrowserRouter>
<Switch>
<Route exact={true} path='/' component={Home} />
<PrivateRoute path='/account' component={Account} />
</Switch>
</BrowserRouter>
);
Upvotes: 4
Reputation:
Don't know if this will help but after searching the entire Internet came to this decision:
https://hackernoon.com/react-authentication-in-depth-part-2-bbf90d42efc9
https://github.com/dabit3/react-authentication-in-depth/blob/master/src/Router.js
import React, { Component } from 'react';
import { Route, Redirect, withRouter } from 'react-router-dom';
import PropTypes from 'prop-types';
import { roleChecker } from '../helpers/formatter';
import { userInfoFetch } from '../api/userInfo';
class PrivateRoute extends Component {
state = {
haveAcces: false,
loaded: false,
}
componentDidMount() {
this.checkAcces();
}
checkAcces = () => {
const { userRole, history } = this.props;
let { haveAcces } = this.state;
// your fetch request
userInfoFetch()
.then(data => {
const { userRoles } = data.data;
haveAcces = roleChecker(userRoles, userRole); // true || false
this.setState({
haveAcces,
loaded: true,
});
})
.catch(() => {
history.push('/');
});
}
render() {
const { component: Component, ...rest } = this.props;
const { loaded, haveAcces } = this.state;
if (!loaded) return null;
return (
<Route
{...rest}
render={props => {
return haveAcces ? (
<Component {...props} />
) : (
<Redirect
to={{
pathname: '/',
}}
/>
);
}}
/>
);
}
}
export default withRouter(PrivateRoute);
PrivateRoute.propTypes = {
userRole: PropTypes.string.isRequired,
};
import React from 'react';
import { Route, Switch } from 'react-router-dom';
import PrivateRoute from '../PrivateRoute';
// pages
import Articles from '../../pages/Articles';
import ArticleCreate from '../../pages/ArticleCreate';
const ArticlesRoute = () => {
return (
<Switch>
<PrivateRoute
exact
path="/articles"
userRole="ArticlesEditor"
component={Articles}
/>
<Route
exact
path="/articles/create"
component={ArticleCreate}
/>
</Switch>
);
};
export default ArticlesRoute;
Upvotes: 1
Reputation: 413
Solution was add second flag: gotUnswerFromServer. Without it component always redirected to "/", without waiting on answer from server.
export default class PrivateRoute extends React.Component {
constructor(props){
super(props);
this.state = {
isLogged: false,
gotUnswerFromServer: false
}
}
componentDidMount(){
const session = read_cookie('session');
fetch(CHECK_TOKEN_API_URL, {
method: 'post',
headers: {'Accept': 'application/json', 'Content-Type': 'application/json'},
body: JSON.stringify({ id: session.id, token: session.token })
}).then( res => {
if(res.ok) this.setState({ gotUnswerFromServer: true, isLogged: true })
})
}
render() {
if( this.state.gotUnswerFromServer ){
if( this.state.isLogged ) return <Route path={this.props.path} component={this.props.component}/>
else return <Redirect to={{pathname: '/', state: { from: this.props.location }}} />
} else return null
}
}
Upvotes: 0