PrivateRoute always redirects to sign in page when refreshing the page; How to wait for useEffect() and localStorage?

Viewed 197

I am implementing security and user management in my application and ran into an issue with react router, Context, localStorage.

With router, I have created a private route and I check if the userID exists on Context or localStorage, if userID does not exist then I re-direct the user to the sign in page. This works great, BUT if I refresh the page, it ALWAYS redirects to the sign in page. Even though the user is logged in, and should exist on localStorage.

I think the issue stems from localStorage and "useEffect()". My theory is that the router redirects BEFORE localStorage (and useEffect) is done checking if the userID exists. Not sure how I can go around this?

Here's the code for my Private Route component"

import {
    Route,
    Redirect,
    RouteProps,
} from 'react-router-dom';
import {MyContextProvider, useMyContext} from '../Context/UserContext';


interface PrivateRouteProps extends RouteProps {
    // tslint:disable-next-line:no-any
    component: any;
    UserID: number;
}

const PrivateRoute = (props: PrivateRouteProps) => {
    const { component: Component, UserID, ...rest } = props;

    console.log("Inside Route: "+ UserID);

    return (
        <Route
            {...rest}
            render={(routeProps) =>
                UserID > 0 ? (
                    <Component {...routeProps} />
                ) : (
                        <Redirect
                            to={{
                                pathname: '/',
                                state: { from: routeProps.location }
                            }}
                        />
                    )
            }
        />
    );
};

export default PrivateRoute;

^ The " console.log("Inside Route: "+ UserID); " prints a 0, so thats why it always redirects to sign in page. This is why I think the useEffect() (shown below in routing) may have something to do with userID not being set properly.

Code for Routing part:

function Routing() { 
  const classes = useStyles();
  const [store, setStore] = useMyContext();

  useEffect(() => { // check if user exists in localstorage, if so, set the values on react Context
    const res = localStorage.getItem("UserID");
    if (res) {
        // since LocalStorage can return a type or null, we must check for nulls and set accordingly.
        const uID = localStorage.getItem("UserID");
        const UserID =  uID !== null ? parseInt(uID) : 0;

        const un = localStorage.getItem("Username");
        const Username = un !== null ? un : '';

        const iA = localStorage.getItem("IsAdmin");
        const isAdmin = iA != 'true' ? true : false;

        const eN = localStorage.getItem("EmailNotifications");
        const emailNotifications = eN != 'true' ? true : false;
       
        setStore({ // update the 'global' context with the logged in user data
            UserID:UserID,
            Username: Username,
            IsAdmin: isAdmin,
            EmailNotifications: emailNotifications
          });
        console.log(UserID);
        console.log(Username);
        console.log(isAdmin);
        console.log("foundUser");
    }
  }, []);

  const id = store.UserID; // get the userID from context; to pass into PrivateRoute


  console.log("ID: ++"+ id);
  return (
      <div className="App">
          <Router>
            <Switch>
              <Route exact path="/" component={Login}/>
              <PrivateRoute path="/dashboard" UserID={id} component={Dashboard}/>
              <PrivateRoute path="/add" UserID={id} component={AddTicket}/>
              <PrivateRoute path="/settings" UserID={id} component={Settings}/>
              <Route component={NotFound}/>
            </Switch>
        </Router>
    </div>
  );
}

export default Routing;
2 Answers

To solve for what I wanted to accomplish, I just added the localStorage retrieval directly inside PrivateRoute (instead of the parent node of PrivateRoute with useEffect)

const PrivateRoute = (props: PrivateRouteProps) => {
const { component: Component, UserID, ...rest } = props;

console.log("Inside Route: "+ UserID);

return (
    <Route
        {...rest}
        render={(routeProps) =>
            localStorage.getItem("UserID")  ? ( // if UserID == 0 when user is NOT logged in. So, if ID == 0, redirect to signinpage
                <Component {...routeProps} />
            ) : (
                    <Redirect
                        to={{
                            pathname: '/',
                            state: { from: routeProps.location }
                        }}
                    />
                )
        }
    />
);

};

Storing the user's id in storage is very bad idea. This is sensitive information. Try to implement logic which will display a spinner or HTML skeleton while waiting to fetch the id from your server or whoever server you are using. The URL might change for a bit but the user will only see the spinner/HTML skeleton and this is way better UX than redirecting and showing pages the user shouldn't see, not to mention the user's id saving in the LocalStorage.

Related