6

I'm working on an app with a login page and the rest of the pages of the app (should be logged in to view). I'm using react-boilerplate. From this example, I edited my asyncInjectors.js file to have redirectToLogin and redirectToDashboard methods:

//asyncInjectors.js
export function redirectToLogin(store) {
  return (nextState, replaceState) => {
    const isAuthenticated = store.getState().get('app').get('isAuthenticated');

    if (!isAuthenticated) {
      replaceState({
        pathname: '/login',
        state: {
          nextPathname: nextState.location.pathname,
        },
      });
    }
  };
}

export function redirectToDashboard(store) {
  return (nextState, replaceState) => {
    const isAuthenticated = store.getState().get('app').get('isAuthenticated');

    if (isAuthenticated) {
      replaceState('/');
    }
  }
}

Then I just set the redirectToLogin as the onEnter of the pages and redirectToDashboard for the login page.

It works fine but when the page is refreshed (F5) when logged in, the login page renders briefly and then renders the actual page. The login page just dispatches an authenticate action in componentWillMount and then redirects in componentDidUpdate:

//login.js
componentWillMount() {
  this.props.dispatch(authenticate());
}

componentDidUpdate(prevProps, prevState) {
  if (this.props.isAuthenticated) {
    const nextPathname = prevProps.location.state ? prevProps.location.state.nextPathname : '/';

    browserHistory.push(nextPathname);
  }
}

The container for the pages also has the same componentWillMount code. Not sure if it's because of the sagas but here's the code:

//sagas.js
export function* login({ user, password }) {
    try {
        const token = yield call(app.authenticate, {
            strategy: 'local',
            user,
            password,
        });

        return token;
    } catch (error) {
        return onError(error);
    }
}

// For page refresh after logging in
export function* authenticate() {
    try {
        const token = yield call(app.authenticate);

        return token;
    } catch (error) {
        return onError(error);
    }
}

export function* logout() {
    try {
        const response = yield call(app.logout);

        return response;
    } catch (error) {
        return onError(error);
    }
}

export function* loginFlow() {
    while (true) {
        const request = yield take(LOGIN_REQUEST);
        const winner = yield race({
            auth: call(login, request.data),
            logout: take(LOGOUT_REQUEST),
        });

        if (winner.auth && winner.auth.accessToken) {
            yield put(actions.setAuthState(true));
        }
    }
}

export function* logoutFlow() {
    while (true) {
        yield take(LOGOUT_REQUEST);
        yield put(actions.setAuthState(false));
        yield call(logout);
        browserHistory.push('/login');
    }
}

export function* authenticateFlow() {
    while (true) {
        yield take(AUTHENTICATE);

        const response = yield call(authenticate);

        if (response && response.accessToken) {
            yield put(actions.setAuthState(true));
        }
    }
}

export default [
    loginFlow,
    logoutFlow,
    authenticateFlow,
];

How do I get rid of the flashing login page?

EDIT: When I tried gouroujo's answer, I couldn't logout.

//asyncInjectors.js
import jwtDecode from 'jwt-decode';

export function redirectToLogin(store) {
    return (nextState, replaceState, callback) => {
        const token = localStorage.token;

        if (token) {
            const jwt = jwtDecode(token);

            if (jwt.exp <= (new Date().getTime() / 1000)) {
                store.dispatch(actions.setAuthState(false));

                replaceState({
                    pathname: '/login',
                    state: {
                        nextPathname: nextState.location.pathname,
                    },
                });
            }
        }

        store.dispatch(actions.setAuthState(true));
        callback();
    };
}

When I hit refresh, the login page doesn't show but now I can't log out.

Community
  • 1
  • 1
dork
  • 3,438
  • 2
  • 20
  • 46
  • To log out you should delete the stored token manualy : `localStorage.removeItem('token')` Moreover you should use `localStorage.getItem('token')` and `localStorage.setItem('token', )` to get and set your token. – gouroujo May 22 '17 at 08:34
  • @gouroujo `app.authenticate` and `app.logout` sets and removes the localStorage token respectively. – dork May 22 '17 at 09:11
  • not verry pretty but : `window.location.reload()` should work and ensure that your application is clean after logout. – gouroujo May 22 '17 at 12:10
  • There's no problem with the actual logging out when it's called. The problem is, the saga itself isn't called – dork May 22 '17 at 13:26

1 Answers1

2

You have two way to avoid flashing the login page on initial render : make your authenticate function synced or wait the answer with a loading page.

1- Check if your token is present and valid (expiration date) client-side to choose if you have to redirect the user to the login or the dashboard page first. When the answer come back from your server you correct your initial guess but in the vaste majority you won't need to.

user landing ->

  • check the token client-side -> redirect to login if needed
  • check the token server-side -> wait answer -> re-redirect if needed

To check the token client-side you have to check the local storage. For example:

class App extends Component {
  requireAuth = (nextState, replace) => {
    if (!localStorage.token) {
      replace({
        pathname: '/login',
        state: { nextPathname: nextState.location.pathname }
      })
    }
  }
  render() {
    return (
      <Router history={browserHistory}>
        <Route path="/login" component={LoginPage} />
        <Route
          path="/"
          component={AppLayout}
          onEnter={this.requireAuth}
        > ... </Route>
     )
   }
 }

If you use a token with a relatively short expiration date, you will also have to check the expiration date, thus decode the token.

try {
      const jwt = JWTdecode(token);
      if (moment().isBefore(moment.unix(jwt.exp))) {
        return nextState;
      } else {
       replace({
        pathname: '/login',
        state: { nextPathname: nextState.location.pathname }
       })
      }
    } catch (e) {
      replace({
        pathname: '/login',
        state: { nextPathname: nextState.location.pathname }
      })
    }

2- Show a loading screen before you receive the answer from the server. add some Css transition effect on opacity to avoid the "flash"

gouroujo
  • 511
  • 3
  • 7
  • In your code snippet, which part of the client-side code should it be in? I tried putting it in `componentWillMount` but it still flashes. The `onEnter` is called first before `componentWillMount`. – dork May 19 '17 at 17:09
  • I have edited my answer to be more specific. But in short: yes you should check the token with onEnter. – gouroujo May 19 '17 at 18:22
  • @dork if you find my answer useful, can you accept it ? thanks ! – gouroujo May 21 '17 at 17:08
  • sorry, I tried your answer but now I can't seem to logout. – dork May 22 '17 at 04:54