-2

I am trying to figure out a way to store the authentication state of a user inside the redux store. Suppose isAuthenticated store the state of user if they are logged-in or not. Now, I have a cookie(httpOnly) sent by the server which remembers the user, so that they don't need to enter there credentials every time they visit the app.
Flow: User some day logged in to the application and didn't logged out and closed the browser. Now, he returns and visit my app. Since, the cookie was there in browser, this will be sent automatically(without user interaction) by the application and if the cookie is valid, the isAuthenticated: true. Very simple requirement.
Tracking the authentication status should be the first thing done by the application, so I put that logic at very first, before the App.js renders.

class App extends Component {

  store = configureStore();

  render() {
    return (
      <Provider store={this.store}>
        <ConnectedRouter history={history}>
          <>
            <GlobalStyle />
              <SiteHeader />
              <ErrorWrapper />
              <Switch>
                <PrivateHomeRoute exact path="/" component={Home} />
                <Route exact path="/login" component={LoginPage} />
                <PrivateHomeRoute path="/home" component={Home} />
             ........code
}

This is the configureStore()

export const history = createBrowserHistory();

const configureStore = () => {
  const sagaMiddleware = createSagaMiddleware();

  const store = createStore(
    rootReducer(history),
    composeEnhancers(applyMiddleware(sagaMiddleware, routerMiddleware(history)))
  );
  sagaMiddleware.run(rootSaga);

  store.dispatch({ type: AUTH.AUTO_LOGIN });

  return store;
};

store.dispatch({ type: AUTH.AUTO_LOGIN }); is the code where I am trying the application to do the auto-login as the first operation in the application. This action is handled by a redux-saga

function* handleAutoLogin() {
  try {
    const response = yield call(autoLoginApi);
    if (response && response.status === 200) {
      yield put(setAuthenticationStatus(true));
    }
  } catch (error) {
    yield put(setAuthenticationStatus(false));
  }
}

function* watchAuthLogin() {
  yield takeLatest(AUTH.AUTO_LOGIN, handleAutoLogin);
}

autoLoginApi is the axios call to the server which will carry the cookie with it. setAuthenticationStatus(true) is action creator which will set the isAuthenticated to true false.

So, yes this is working BUT not as expected. Since, the app should first set the isAuthenticated first and then proceed with the App.js render(). But, since setting the isAuthenticated take some seconds(api call), the application first renders with the isAuthenticated: false and then after the AUTH.AUTO_LOGIN gets completed, then the application re-render for authenticaed user.

What's the problem then? For the normal component it may not be the problem, e.g this SiteHeader component

class SiteHeader extends React.Component {
  render() {
    const { isLoggedIn } = this.props;

    if (isLoggedIn === null) {
      return "";
    } else {
      if (isLoggedIn) {
        return (
          <LoggedInSiteHeader />
        );
      } else {
        return (
          <LoggedOutSiteHeader />
        );
      }
    }
  }
}

const mapStateToProps = ({ auth, user }) => ({
  isLoggedIn: auth.isLoggedIn,
});

export default connect(
  mapStateToProps,
  null
)(SiteHeader);

But, this solution doesn't work for the Custom routing.

const PrivateHomeRoute = ({ component: ComponentToRender, ...rest }) => (
  <Route
    {...rest}
    render={props =>
      props.isLoggedIn ? (
        <ComponentToRender {...props} />
      ) : (
        <Redirect to="/login" />
      )
    }
  />
);

const mapStateToProps = auth => ({
  isLoggedin: auth.isLoggedIn
});

export default connect(
  mapStateToProps,
  null
)(PrivateHomeRoute);

PrivateHomeRoute gets resolved before the redux store gets updated, hence the Route always goes to "/login".

I am looking for a solution, where the application doesn't proceed further until the authentication action doesn't complete. But, I am no clue what and where to put that code?

Few things I tried:

  1. async await on configureStore() - Error came
  2. async await on App.js - Error

PS: Libraries I am using redux, redux-saga,react-router-dom, connected-react-router, axios

The Coder
  • 3,447
  • 7
  • 46
  • 81
  • Perhaps this answer will have a similar solution for you? https://stackoverflow.com/questions/31723095/how-to-wait-for-ajax-response-and-only-after-that-render-the-component – Jonathan Kempf Mar 13 '19 at 12:37

1 Answers1

0

One way I figured out:
Create a separate component MyRouteWrapper which will return the routes based on the isLoggedIn status. To, resolve the issue I stop the routes to render until the auto-login changes the isLoggedIn state.

I set the default state of isLoggedIn to null. Now, if the state is null the MyRouteWrapper will return an empty string, and once the state gets changes to true/false, it will return the routes, and then respective components get rendered.

I changed my App.js

const store = configureStore();

class App extends Component {
  render() {
    return (
      <Provider store={store}>
        <ConnectedRouter history={history}>
          <MyRouteWrapper />
        </ConnectedRouter>
      </Provider>
    );
  }
}    
export default App;

The component which make sure to return the Route only when the state gets changed to true/false

const MyRouteWrapper = props => {
  if (props.isLoggedIn === null) {
    return "";
  } else {
    return (
      <>
        <GlobalStyle />
        <SiteHeader />
        <ErrorWrapper />
        <Switch>
          <ProtectedHomeRoute
            exact
            path="/"
            component={Home}
            isLoggedIn={props.isLoggedIn}
          />
          <Route path="/profile/:id" component={Profile} />
          <Route path="/login" component={LoginPage} />
        </Switch>
      </>
    );
  }
};

const mapStateToProps = ({ auth }) => ({
  isLoggedIn: auth.isLoggedIn
});

export default connect(mapStateToProps)(MyRouteWrapper);

This solved the issue.

I am still curious to know the solutions(better) anyone have in there mind.

The Coder
  • 3,447
  • 7
  • 46
  • 81