React flashing Private Route when User is not authenticated

Viewed 2014

I'm using react-router-dom to secure the entire application. All routes are protected under a ProtectedRoute component (see code below), which redirects to an external url, a single-sign-on (SSO) page if the user is not logged in.

Problem:

When the user goes to '/home', they get a brief glimpse (a "flash") of the protected route before getting redirected to 'external-login-page.com/' (the login page). How do I avoid the flashing so that the user only sees the login page?

export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
  isAuthenticated,
  ...rest
}) => {
  if (!isAuthenticated) { // redirect if not logged in
    return (
      <Route
        component={() => {
          window.location.href = 'http://external-login-page.com/';
          return null;
        }}
      />
    );
  } else {
    return <Route {...rest} />;
  }
};
4 Answers

window.location.href can be called earlier to prevent flashing. Also in your specific case what you probably want is to render nothing at all when the user is not authenticated.

The code may look like this:

export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
  isAuthenticated,
  ...rest
}) => {
  if (!isAuthenticated) { // redirect if not logged in
    window.location.href = 'http://external-login-page.com/';
    return null;
  } else {
    return <Route {...rest} />;
  }
};

You might consider the Redirect component

export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
  isAuthenticated,
  ...rest
}) => {
  if (!isAuthenticated) { 
    return <Redirect to='https://external-login-page.com/' />
  } else {
    return <Route {...rest} />;
  }
};

I would guess that invoking window directly + return null is rendering the React app for a split second before the page reloads.

You can use the Redirect component in a simpler way like this.

  export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
      isAuthenticated,
       children,
      ...rest
    }) => {
         return <Route {...rest} render={() => isAuthenticated ? children : <Redirect to='http://external-login-page.com/' />}
    }

Posting the solution that eventually worked for me: instead of blocking by Router, block by App.

The key is to split your App into two components, AuthenticatedApp and UnauthenticatedApp. From there, lazy load the correct component depending on the user's level of access. This way, if they're not authorized, their browser won't even load AuthenticatedApp at all.

  • AuthenticatedApp is a component to your entire app, providers, routers, etc. Whatever you had in App.tsx originally should go here.
  • UnauthenticatedApp is a component that you want your users to see when they're not allowed to access the application. Something like "Not authorized. Please contact admin for help."

App.tsx

const AuthenticatedApp = React.lazy(() => import('./AuthenticatedApp'));
const UnauthenticatedApp = React.lazy(() => import('./UnauthenticatedApp'));

// Dummy function to check if user is authenticated
const sleep = (time) => new Promise((resolve) => setTimeout(resolve, time));
const getUser = () => sleep(3000).then(() => ({ user: '' }));

const App: React.FC = () => {
  // You should probably use a custom `AuthContext` instead of useState,
  // but I kept this for simplicity.
  const [user, setUser] = React.useState<{ user: string }>({ user: '' });

  React.useEffect(() => {
    async function checkIfUserIsLoggedInAndHasPermissions() {
      let user;
      try {
        const response = await getUser();
        user = response.user;
        console.log(user);
        setUser({ user });
      } catch (e) {
        console.log('Error fetching user.');
        user = { user: '' };
        throw new Error('Error authenticating user.');
      }
    }
    checkIfUserIsLoggedInAndHasPermissions();
  }, []);

  return (
    <React.Suspense fallback={<FullPageSpinner />}>
      {user.user !== '' ? <AuthenticatedApp /> : <UnauthenticatedApp />}
    </React.Suspense>
  );
};

export default App;

Read Kent C Dodd's great post about it here [0]!

EDIT: Found another great example with a similar approach, a bit more complex - [1]

[0] https://kentcdodds.com/blog/authentication-in-react-applications?ck_subscriber_id=962237771 [1] https://github.com/chenkie/orbit/blob/master/orbit-app/src/App.js

Related