Dmitry Klymenko
Dmitry Klymenko

Reputation: 413

React. Private router with async fetch request

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

Answers (5)

giggi__
giggi__

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

cravter
cravter

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

oleksiizapara
oleksiizapara

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

user10276110
user10276110

Reputation:

async private router react

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

my case was to redirect from hidden to home page if user did not have required role:

PrivateRoute

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,
};

ArticlesRoute

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

Dmitry Klymenko
Dmitry Klymenko

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

Related Questions