Search code examples
javascriptreactjsfirebasefirebase-authenticationreact-router-dom

Login page is displayed when I refresh Homepage in React with Firebase Auth


I implemented Firebase authentication in my app by the docs. I have a login screen and a landing screen which is accessed only if you are logged in.

For some reason, when I am logged in and I refresh my landing screen, it redirects me to login screen and then back to the landing screen. I'm having a hard time figuring out why.

Here is some code that could help you out:

index.js (imports and other stuff is left out)

root.render(
  <AuthProvider>
    <Router>
      <Toaster />
      <AppRouter />
    </Router>    
  </AuthProvider>    
);

AuthProvider.js

import React, { useEffect, useState } from "react";
import { onAuthStateChanged } from "firebase/auth";
import { FirebaseAuth } from "./firebase/config";

export const AuthContext = React.createContext();

export const AuthProvider = ({ children }) => {
  const [currentUser, setCurrentUser] = useState(null);

  useEffect(() => {
    onAuthStateChanged(FirebaseAuth, user => {
      if (user) {
        setCurrentUser(user)
      } else {
        setCurrentUser(null)
      }
    })
  }, []);

  return (
    <AuthContext.Provider value={{ currentUser }}>
      {children}
    </AuthContext.Provider>
  );
};

And here is the AppRouter.js

import { useContext } from "react";
import Landing from "./screens/Landing"
import {
  Navigate,
  BrowserRouter,
  Routes,
  Route
} from "react-router-dom";
import Login from "./screens/auth/Login";
import { AuthContext } from './AuthProvider';

const AppRouter = () => {
  const { currentUser } = useContext(AuthContext);

  return (
    <Routes>
      <Route
        path="/"
        element={!!currentUser ? <Landing /> : <Navigate to="/login" />}
        exact
      />
      <Route
        path="/login"
        element={!currentUser ? <Login /> : <Navigate to="/" />}
        exact
      />
    </Routes>
  )
}

export default AppRouter;

Solution

  • The initial currentUser state matches the confirmed "unauthenticated" user state, e.g. null. When the app mounts, this initial falsey value will be used when rendering the routes.

    Use an initial currentUser state value that is neither "authenticated" nor "unauthenticated" and do an explicit check for this and conditionally return early so the app neither renders the protected content nor the redirect.

    export const AuthProvider = ({ children }) => {
      const [currentUser, setCurrentUser] = useState(); // <-- initially undefined
    
      useEffect(() => {
        onAuthStateChanged(FirebaseAuth, user => {
          if (user) {
            setCurrentUser(user);
          } else {
            setCurrentUser(null);
          }
        });
      }, []);
    
      return (
        <AuthContext.Provider value={{ currentUser }}>
          {children}
        </AuthContext.Provider>
      );
    };
    
    const AppRouter = () => {
      const { currentUser } = useContext(AuthContext);
    
      if (currentUser === undefined) {
        return null; // <-- or loading indicator/spinner/etc
      }
    
      return (
        <Routes>
          <Route
            path="/"
            element={!!currentUser ? <Landing /> : <Navigate to="/login" />}
          />
          <Route
            path="/login"
            element={!currentUser ? <Login /> : <Navigate to="/" />}
          />
        </Routes>
      );
    }
    

    For a more conventional method of implementing route protection see my answer here.