DEV Community

Protected Routes with React Function Components

mychal on January 21, 2020

Protected routes allow us to ensure only logged in users can access certain parts of our site that may contain private user information. In this ...
Collapse
 
juanortizoviedo profile image
juanortizoviedo

Not work if i refresh '/dashboard'

Collapse
 
franciscobenedict profile image
Francisco Benedict (DfMS) • Edited

@juanortizoviedo - I had the exact same problem so once the web browser is refreshed whilst the URL is /dashboard, the page is automatically redirected to /unauthorized which makes sense as this is specified in ProtectedRoute.

I tried refreshing the browser whilst on /unauthorized page and it worked as expected (reloaded /unauthorized as normal).

I've since created a fresh create-react-app project with firebase and authenticating a user with either email or google provider and this exact same refresh issue is happening. It's almost as if the user is authenticated quickly enough on page load so the browser redirects the specified page or back to the root (/) URL.

So if a user (authenticated or not authenticated) refreshes the browser on a non-protected page, everything is fine but they are redirected if already on a protected page.

Have you managed to figure out how to overcome this issue? Any help would be greatly appreciated @mychal

Collapse
 
anthonyhumphreys profile image
Anthony Humphreys

Have a read over the answer here: stackoverflow.com/questions/279283...

Client side routing is hard! Basically what you're seeing is the server try to find something at /route, when your SPA loads from /index.html - /route doesn't 'really' exist - until your index.html is read and your JS kicks in. It really blows your mind until you get it. Using react router you can evade this issue by using hashrouting

Collapse
 
johnmarsden24 profile image
Jonny • Edited

@franciscobenedict I had this exact same issue. Firebase will persist an authenticated users details in local storage, however to retrieve them initially is asynchronous which as you've experienced doesn't work when refreshing the page and you get taken to a login screen, etc.

As we will be wanting to know about this user state across multiple components its advised to wrap all this logic into a context provider. To start solving this we can utilise onAuthStateChanged from Firebase which is the recommend way of retrieving a user as it allows Firebase to initialise, it will provide either the user object or null in the callback. What we also need is a loading state, which is true on initial launch and when the user has been loaded or if there isn't one we can stop loading. To get this behaviour we can wrap onAuthStateChanged in a promise which resolves the user and call it on initial launch using useEffect. We can then wait for this promise to settle before we manage our final loading state. Finally we can decide what to do once we've finished loading and we either have or don't have a user.

Here's my code:

Helper function

const getCurrentUser = () =>
  new Promise((res, rej) => {
    functionsApp.auth().onAuthStateChanged((user) => res(user));
  });
Enter fullscreen mode Exit fullscreen mode

AuthContext.js

const AuthContext = createContext({
  user: null,
  loadingUser: true,
  setCurrentUser: () => {},
  unsetCurrentUser: () => {},
});

export const AuthContextProvider = (props) => {
  const [user, setUser] = useState(null);
  const [loadingUser, setLoadingUser] = useState(true);

  useEffect(() => {
    getCurrentUser().then((user) => {
      setUser(user);
      setLoadingUser(false);
    });
  }, []);

  const setCurrentUser = (user) => setUser(user);
  const unsetCurrentUser = () => signOutUser().then(() => setUser(null));

  const contextValue = {
    user,
    loadingUser,
    setCurrentUser,
    unsetCurrentUser,
  };

  return (
    <AuthContext.Provider value={contextValue}>
      {props.children}
    </AuthContext.Provider>
  );
};
Enter fullscreen mode Exit fullscreen mode

ProtectedRoute.js

function ProtectedRoute({ component: Component, ...restOfProps }) {
  const { user, loadingUser } = useContext(AuthContext);

  if (loadingUser) {
    return <p>Loading..</p>;
  }

  return (
    <Route
      {...restOfProps}
      render={(props) =>
        user ? <Component {...props} /> : <Redirect to="/auth" />
      }
    />
  );
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
2ndplayer profile image
Chris Camp

The issue seems to be as simple as the demo application doesn't have a persisting state of the user's auth. Since it's just artificially creating auth for a user and storing it in memory. The state of the user being logged in can't survive a page refresh. So naturally when you login and going to dashboard via the on page links it works but when you do a page refresh the user's current logged in state is lost from memory and defaults back to its original state of false. Look at the React Developer Tools and monitor the components state regarding the user's auth. A solution to this in a real application would be to have some state management system like context or Redux to persist the state so the accurate page shows when expected.

Collapse
 
faizanh profile image
Faizan • Edited

I came across this issue when working with protected routes. The reason as you've already figured out, is due to the user state initially being sent to our components (defaults as false), so when it hits our protected route, it redirects the user to 401 unauthorised. What I did to solve this issue was check the user state before returning the routes. To "load" the app before it actually hit the router. I've attached some sample code, hopefully helps someone stuck with this.

gist.github.com/FaizanH/88ede257df...

Collapse
 
ahmaddevsinc profile image
ahmad-devsinc

There must be a typo

Collapse
 
mabradbirney profile image
maBradBirney • Edited

What a wonderful guide!
There were a couple things that caught me while following along:

  1. You misspelled "components" in "import Unauthorized from './comoponents/Unauthorized';"
  2. Also it seems like you don't need the prop "handleLogin" in the Route component for "<Route exact path='/' handleLogin={handleLogin} render={..." unless it's there for some reason I'm unaware of.

Thank you so much for this article though! It was very helpful for me!

Collapse
 
sevenzark profile image
Dave M • Edited

Unfortunately when I try this approach and hit the protected route, I get a show-stopping error: 'Invariant failed: You should not use <Route> outside a <Router>'. Seems like it doesn't like me composing something around the Route component, but I don't know why you and I get different results for that.

Collapse
 
lydstyl profile image
lydstyl

Thank you :-)

Collapse
 
mychal profile image
mychal

No problem, thanks for reading!

Collapse
 
code4kongo profile image
Code4kongo

great article
i have tried to do the same thing in my application but i have a problem
the data that i passed to the ProtectedRoutes are undefined i don't know why

at first the date are present but once i click on the login button which must change the Auth state to true after an API call.
but once i receive the data they are lost in the ProtectedRoute and are set to undefined

Collapse
 
niyongaboeric profile image
NiyongaboEric

Thanks for sharing your knowledge. I don't understand why implementing this idea in my other project took me a long time. You made it so simple to protect private routes in ReactJS. Thanks again.

Collapse
 
onlyinspace profile image
OnlyInSpace • Edited

Anyway to implement this with page refresh?

Collapse
 
luigi_leano profile image
Luis Uriel Leaño

Thank you , it was very helpful

Collapse
 
jy03187487 profile image
Zhe Bao

Very useful, thank you very much!

Collapse
 
franciscobenedict profile image
Francisco Benedict (DfMS)

This is really good guide. I quite enjoyed it. Very easy to follow and concise too. Well done @mychal

Collapse
 
ahmadanis profile image
AHMAD

great job.... thanks

Collapse
 
ahmaddevsinc profile image
ahmad-devsinc

This is a highly understandable guide. Kudos to you sir

Collapse
 
josemfcheo profile image
José Martínez

Thanks for the knowledge!

Collapse
 
sumit134coder profile image
sumit mehra

what if someone just add /dashboard in url? Thats the problem i have to deal with. Any solution to it?

Collapse
 
srisrinu_ profile image
srinivas-challa1

After so many days of struggle finally i found it

Collapse
 
asifmeem profile image
Asif Foysal Meem

Thank you for the detailed breakdown of Protected Routing Mychal!

Collapse
 
terrodar profile image
Terrodar

Thanks! I was struggling with other tutorials to understand the HOC that abstract the logic of protecting the components but now I understand thanks to you.