I have implemented Firebase Auth (Sign In With Google) in a MERN stack app.
The /admin
client-side route is a protected route. After I log in, I see the displayName of the logged in user, as shown below:
Admin.js
import React, { useContext } from "react";
import { Link } from "react-router-dom";
import { useNavigate } from "react-router-dom";
import { getAuth, signOut } from "firebase/auth";
import { useFetchAllPosts } from "../hooks/postsHooks";
import Spinner from "../sharedUi/Spinner";
import { PencilAltIcon } from "@heroicons/react/outline";
import { UserContext } from "../context/UserProvider";
const Admin = () => {
const { data: posts, error, isLoading, isError } = useFetchAllPosts();
const auth = getAuth();
const navigate = useNavigate();
const { name } = useContext(UserContext);
const handleLogout = () => {
signOut(auth).then(() => {
navigate("/");
});
};
return (
<>
<h2>Hello {name}</h2>
<button className="border" onClick={handleLogout}>
Logout
</button>
<div>
{isLoading ? (
<Spinner />
) : isError ? (
<p>{error.message}</p>
) : (
posts.map((post) => (
<div
key={post._id}
className="flex w-80 justify-between px-6 py-2 border rounded mb-4 m-auto"
>
<Link to={`/posts/${post._id}`}>
<h2>{post.title}</h2>
</Link>
<Link to={`/posts/edit/${post._id}`}>
<PencilAltIcon className="w-5 h-5" />
</Link>
</div>
))
)}
</div>
</>
);
};
export default Admin;
App.js
import React from "react";
import { Routes, Route } from "react-router-dom";
import Home from "./components/screens/Home";
import PostCreate from "./components/screens/PostCreate";
import SinglePost from "./components/screens/SinglePost";
import Admin from "./components/screens/Admin";
import PostEdit from "./components/screens/PostEdit";
import Login from "./components/screens/Login";
import PrivateRoute from "./components/screens/PrivateRoute";
const App = () => {
return (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/admin" element={<PrivateRoute />}>
<Route path="/admin" element={<Admin />} />
</Route>
<Route path="/posts/create" element={<PostCreate />} />
<Route path="/posts/:id" element={<SinglePost />} />
<Route path="/posts/edit/:id" element={<PostEdit />} />
<Route path="/login" element={<Login />} />
</Routes>
);
};
export default App;
PrivateRoute.js
import React from "react";
import { Navigate, Outlet, useLocation } from "react-router-dom";
import { getAuth } from "firebase/auth";
const PrivateRoute = () => {
const location = useLocation();
const auth = getAuth();
console.log(auth.currentUser);
return auth.currentUser ? (
<Outlet />
) : (
<Navigate to="/login" state={{ from: location }} />
);
};
export default PrivateRoute;
Now, if I refresh this page, I go back to the /login
route.
However, this should not happen, because if I go to the root /
route, I see the displayName of the current user.
If I refresh the page while on the root /
route, I still see the displayName of the current user.
So, my question is why am I getting redirected to the /login
page after I refresh the page on the /admin
route? I am logged in, so I should remain on the Admin
page.
The logic of whether a user is logged-in or not is implemented in the UserProvider
component:
UserProvider.js
import React, { useState, useEffect, createContext } from "react";
import { getAuth, onAuthStateChanged } from "firebase/auth";
export const UserContext = createContext();
const UserProvider = ({ children }) => {
const [name, setName] = useState(null);
const auth = getAuth();
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (user) => {
if (user) {
console.log(user);
setName(user.displayName);
} else {
setName(null);
}
});
return unsubscribe;
}, [auth]);
const user = {
name,
setName,
};
return <UserContext.Provider value={user}>{children}</UserContext.Provider>;
};
export default UserProvider;
CodePudding user response:
The Firebase Authentication SDK automatically restores the user upon reloading the page/app. This does require a call to the server - a.o. to check if the account hasn't been disabled. While that verification is happening, auth.currentUser
will be null.
Once the user account has been restored, the auth.currentUser
will get a value, and (regardless of whether the restore succeeded or failed) any onAuthStateChanged
listeners are called.
What this means is that you should not check the auth.currentUser
value in code that runs immediately on page load, but should instead react to auth state changes with a listener.
If you need to route the user based on their authentication state, that should happen in response to the auth state change listener too. If you want to improve on the temporary delay that you get from this, you can consider implementing this trick that Firebaser Michael Bleigh talked about at I/O a couple of years ago: https://www.youtube.com/watch?v=NwY6jkohseg&t=1311s
CodePudding user response:
I figured out a solution. I installed the react-firebase-hooks
npm module and used the useAuthState
hook to monitor the authentication status.
PrivateRoute.js
import React from "react";
import { Navigate, Outlet, useLocation } from "react-router-dom";
import { getAuth } from "firebase/auth";
import { useAuthState } from "react-firebase-hooks/auth";
const PrivateRoute = () => {
const location = useLocation();
const auth = getAuth();
const [user, loading] = useAuthState(auth);
if (loading) {
return "Loading...";
}
return user ? (
<Outlet />
) : (
<Navigate to="/login" state={{ from: location }} />
);
};
export default PrivateRoute;