Can't access protected routes with Firebase onAuthStateChanged [duplicate] - reactjs

I'm using firebase for user authentication and react-router-dom 6 for private routes. The "/account" page is protected and wrapped inside private routes. I have a Nav component, which has an icon that redirects to the "/account" page, the code is as follows:
export default function Nav() {
const navigate = useNavigate();
return (
<>
<nav className='navbar'>
<div className="account-container">
<RiUser3Fill className='account-icon menu-icon' onClick={()=>{navigate("account")}}/>
<BsCartFill className='cart-icon menu-icon' onClick={()=>{navigate('cart')}}/>
</div>
</nav>
<Outlet/>
</>
)
}
When I click on the account icon and the user is logged in, the page would redirect to the protected account page. But the problem is, when I refresh the page at "/account", or type in the URL to get to "/account", the page would always be redirected to "/signin" page even when the user is already signed in. Below are my other components:
App.js:
function App() {
return <BrowserRouter>
<AuthProvider>
<Routes>
<Route path="/" element={<Nav/>}>
<Route element={<PrivateRouter/>}>
<Route path="/account" element={<Account/>}/>
<Route path="/cart" element={<Cart/>}/>
</Route>
<Route path='/signup' element={<Signup/>}/>
<Route path='/signin' element={<Signin/>}/>
</Route>
</Routes>
</AuthProvider>
</BrowserRouter>
}
PrivateRouter.jsx:
export default function PrivateRouter() {
const {currentUser} = useAuth();
const location = useLocation();
if(!currentUser) return <Navigate state={{from:location}} to="/signin"/>
return <Outlet />
}
AuthContext.js:
import React, {useContext, useEffect, useState} from 'react';
import { signInWithEmailAndPassword, createUserWithEmailAndPassword, signOut, onAuthStateChanged,
passwo } from 'firebase/auth';
import { auth } from '../utility/firebase';
const AuthContext = React.createContext();
export function useAuth(){
return useContext(AuthContext);
}
export default function AuthProvider({children}) {
const [currentUser, setCurrentUser] = useState();
useEffect(()=>{
const unsub = onAuthStateChanged(auth,user=>{
setCurrentUser(user);
})
return unsub;
},[])
function signin(email,password){
return signInWithEmailAndPassword(auth,email,password);
}
function signup(email,password){
return createUserWithEmailAndPassword(auth,email,password);
}
function signout(){
return signOut(auth);
}
const values = {
currentUser,
signin,
signup,
signout
}
return <AuthContext.Provider value={values}>
{children}
</AuthContext.Provider>;
}

The currentUser initial value is undefined until updated by the auth check in the useEffect.
const [currentUser, setCurrentUser] = useState(); // <-- undefined
useEffect(() => {
const unsub = onAuthStateChanged(auth, user => {
setCurrentUser(user); // <-- sets user state after initial render
});
return unsub;
}, []);
So when refreshing the page, i.e. remounting the app, the currentUser condition in the auth check is falsey and user is bounced to login/signin page.
If the currentUser is still undefined, i.e. the app hasn't determined/confirmed either way a user's authentication status, you should return null and not commit to redirecting or allowing access through to the routed component.
export default function PrivateRouter() {
const { currentUser } = useAuth();
const location = useLocation();
if (currentUser === undefined) return null; // or loading spinner, etc...
return currentUser
? <Outlet />
: <Navigate to="/signin" replace state={{ from: location }} />;
}

Related

React Router Outlet and Protected Outlet does not working

I want to render outlets based on user login state. The problem is the oulet never get rendered. Only parent element is rendered.I tried using "/:username/boards/*" for path. But that didn't work either.
App.js
<Route element={<ProtectedOutlet />}>
<Route path="/:username/boards" element={<UserHome logOut={actions.logOut} getAllBoards={actions.getAllBoards} />} >
<Route path="b" element={<Board />} />
</Route>
</Route>
ProtectedOutlet.js
import React from "react";
import { Navigate, Outlet } from "react-router-dom";
import { useSelector } from "react-redux";
const ProtectedOutlet = () => {
const isLoggedIn = useSelector(state => state.auth.isLoggedIn);
return isLoggedIn ? <Outlet /> : <Navigate to="/login" />;
}
export default ProtectedOutlet;
UserHome.js
const UserHome = (props) => {
return (
<div className="user-home">
<Navbar />
<Outlet />
</div>
)
}
Problem solved. I changed my codes in ProtectedOutlet
from
const ProtectedOutlet = () => {
const isLoggedIn = useSelector(state => state.auth.isLoggedIn);
return isLoggedIn ? <Outlet /> : <Navigate to="/login" />;
}
to
const ProtectedOutlet = () => {
const { isLoggedIn, user } = useSelector(state => state.auth);
if (!isLoggedIn && !user) return <h1>Loading</h1>
return isLoggedIn ? <Outlet /> : <Navigate to="/login" />;
}
And I've other route for unauthenticated routes like (/login,/register, etc..) called PublicOutlet.js.
PublicOutlet.js
import React from "react";
import { Navigate, Outlet } from "react-router-dom";
import { useSelector } from "react-redux";
const PublicOutlet = () => {
const { isLoggedIn, user } = useSelector(state => state.auth);
return isLoggedIn ? <Navigate to={`/${user.username}/boards`}
replace={true} /> : <Outlet />;
}
export default PublicOutlet;
The problem is when I enter url in url bar, the protectedoutlet component check if the user is logged in or not. When it is rendered the first time, isLoggedIn state is not updated yet. so it get navigated to /login route. Then /login route is wrapped in publicoutlet component. When publicoutlet component check if the user is is logged in, isLoggedIn state changed to true. So, it get redirected to /${user.username}/boards. It took me 2days to find out the problem. LOL

Why is my React Authentication component using AWS Amplify being rendered infinitely when using React Router V6 to protect routes

I am trying to create my own custom authentication using React, AWS Amplify, and React Router V6, and my goal is to protect certain routes so users that are not logged in can't access them.
My code is here:
import './App.css';
import { ThemeProvider, createTheme } from '#mui/material/styles';
import Navbar from './components/Navbar/Navbar';
import Dashboard from './components/Dashboard/Dashboard';
import Reports from './components/Reports/Reports';
import Patients from './components/Patients/Patients';
import { BrowserRouter, Navigate, Outlet, Route, Routes, useLocation, } from 'react-router-dom';
import React, { useEffect, useState } from 'react';
import '#aws-amplify/ui-react/styles.css';
import Signup from './components/Signup/Signup';
import Signin from './components/Signin/Signin';
import { Auth } from 'aws-amplify';
const darkTheme = createTheme({
palette: {
mode: 'dark',
}
});
const useAuth = () => {
const [user, setUser] = useState(null);
useEffect(() => {
const fetchUser = async () => {
const response = await Auth.currentAuthenticatedUser();
setUser(response);
}
fetchUser();
})
return { user };
}
function RequireAuth() {
const auth = useAuth();
const location = useLocation();
console.log(auth);
return (
auth
? <Outlet/>
: <Navigate to='/signin' state={{ from: location}} replace/>
);
}
const App = () => {
return (
<ThemeProvider theme={darkTheme}>
<BrowserRouter>
<Routes>
<Route element={<RequireAuth />}>
<Route path="/" element={<Navbar />}>
<Route path="dashboard" element={<Dashboard />} />
<Route path="patients" element={<Patients />} />
<Route path="reports" element={<Reports />} />
</Route>
</Route>
<Route path="/signin" element={<Signin />} />
<Route path="/signup" element={<Signup />} />
</Routes>
</BrowserRouter>
</ThemeProvider>
);
}
export default App;
I have spent the whole day trying countless methods but I keep getting the same results. What I'm trying to do is protect 3 routes Dashboard, Patients, and Reports. Whenever I click the sign-in button, I get around 500 logs of the current user as shown below:
Does anyone know what is triggering this component to re-render infinitely (hence executing my console.log(user) function), and is there any solution to fix this?
It appears the useEffect hook is missing a dependency array, so its callback is triggered each render cycle. Since the effect updates state, it triggers a rerender.
Add a dependency array.
useEffect(() => {
const fetchUser = async () => {
const response = await Auth.currentAuthenticatedUser();
setUser(response);
}
fetchUser();
}, []);
If you need to run this effect more than once when the routes are mounted, then you may need to add a dependency, like location if/when the route path changes.
Example:
const { pathname } = useLocation();
useEffect(() => {
const fetchUser = async () => {
const response = await Auth.currentAuthenticatedUser();
setUser(response);
}
fetchUser();
}, [pathname]);
Since the useEffect hook runs at the end of the initial render cycle you may want to also conditionally wait to render the outlet or redirect until the user state is populated.
Example:
const useAuth = () => {
const [user, setUser] = useState(); // <-- initially undefined
useEffect(() => {
const fetchUser = async () => {
const response = await Auth.currentAuthenticatedUser();
setUser(response);
}
fetchUser();
}, []);
return { user };
}
...
function RequireAuth() {
const auth = useAuth();
const location = useLocation();
if (auth === undefined) {
return null; // or loading indicator, etc...
}
return (
auth
? <Outlet/>
: <Navigate to='/signin' state={{ from: location }} replace/>
);
}

React Router Dom 6 page redirected to sign in page even when user is signed in

I'm using firebase for user authentication and react-router-dom 6 for private routes. The "/account" page is protected and wrapped inside private routes. I have a Nav component, which has an icon that redirects to the "/account" page, the code is as follows:
export default function Nav() {
const navigate = useNavigate();
return (
<>
<nav className='navbar'>
<div className="account-container">
<RiUser3Fill className='account-icon menu-icon' onClick={()=>{navigate("account")}}/>
<BsCartFill className='cart-icon menu-icon' onClick={()=>{navigate('cart')}}/>
</div>
</nav>
<Outlet/>
</>
)
}
When I click on the account icon and the user is logged in, the page would redirect to the protected account page. But the problem is, when I refresh the page at "/account", or type in the URL to get to "/account", the page would always be redirected to "/signin" page even when the user is already signed in. Below are my other components:
App.js:
function App() {
return <BrowserRouter>
<AuthProvider>
<Routes>
<Route path="/" element={<Nav/>}>
<Route element={<PrivateRouter/>}>
<Route path="/account" element={<Account/>}/>
<Route path="/cart" element={<Cart/>}/>
</Route>
<Route path='/signup' element={<Signup/>}/>
<Route path='/signin' element={<Signin/>}/>
</Route>
</Routes>
</AuthProvider>
</BrowserRouter>
}
PrivateRouter.jsx:
export default function PrivateRouter() {
const {currentUser} = useAuth();
const location = useLocation();
if(!currentUser) return <Navigate state={{from:location}} to="/signin"/>
return <Outlet />
}
AuthContext.js:
import React, {useContext, useEffect, useState} from 'react';
import { signInWithEmailAndPassword, createUserWithEmailAndPassword, signOut, onAuthStateChanged,
passwo } from 'firebase/auth';
import { auth } from '../utility/firebase';
const AuthContext = React.createContext();
export function useAuth(){
return useContext(AuthContext);
}
export default function AuthProvider({children}) {
const [currentUser, setCurrentUser] = useState();
useEffect(()=>{
const unsub = onAuthStateChanged(auth,user=>{
setCurrentUser(user);
})
return unsub;
},[])
function signin(email,password){
return signInWithEmailAndPassword(auth,email,password);
}
function signup(email,password){
return createUserWithEmailAndPassword(auth,email,password);
}
function signout(){
return signOut(auth);
}
const values = {
currentUser,
signin,
signup,
signout
}
return <AuthContext.Provider value={values}>
{children}
</AuthContext.Provider>;
}
The currentUser initial value is undefined until updated by the auth check in the useEffect.
const [currentUser, setCurrentUser] = useState(); // <-- undefined
useEffect(() => {
const unsub = onAuthStateChanged(auth, user => {
setCurrentUser(user); // <-- sets user state after initial render
});
return unsub;
}, []);
So when refreshing the page, i.e. remounting the app, the currentUser condition in the auth check is falsey and user is bounced to login/signin page.
If the currentUser is still undefined, i.e. the app hasn't determined/confirmed either way a user's authentication status, you should return null and not commit to redirecting or allowing access through to the routed component.
export default function PrivateRouter() {
const { currentUser } = useAuth();
const location = useLocation();
if (currentUser === undefined) return null; // or loading spinner, etc...
return currentUser
? <Outlet />
: <Navigate to="/signin" replace state={{ from: location }} />;
}

protected route not rendering protected componet after authentication

So I have a react app and I want to redirect users to a dashboard after authentication. I am using react-router-dom. The surprising thing is that after all setup and I try to access the protected route without authentication it redirects me back to the home page which works well also, when I console log to check if user verification function works. it returns authenticated user which means that is also working well. But when I sign in as a user react is supposed to render the dashboard but unfortunately it doesn't. I am so confused at the moment. Please some assistance would be highly appreciated. Thanks
ProtectedRoute.js
import React from 'react';
import PropTypes from 'prop-types';
import {Route,Redirect} from 'react-router-dom';
const ProtectedRoute = ({isAuth:isLoggedin,component:Component,...rest})=>{
console.log(Component,isLoggedin)
return (
<Route
{...rest}
render={(props)=>{
if(isLoggedin){
return <Component />
}else{
return(
<Redirect to={{
pathname:'/',
state: {from: props.location}
}}
/>
)
}
}
}
/>
)
}
export default ProtectedRoute;
ProtectedRoute.propTypes={
isAuth: PropTypes.bool.isRequired
}
App.js
const App = ()=>{
const [isClicked, setIsClicked]= useState(false);
const [username,setUsername] = useState()
const [pass,setPass] = useState();
const [isLoggedIn,setIsLoggedIn] = useState(false);
const [authUser,setAuthUser] = useState({})
const disableScroll =()=>{
document.body.style.overflow = 'hidden';
document.querySelector('html').scrollTop = window.scrollY;
}
const enableScroll=()=>{
document.body.style.overflow = null;
}
const handleLogin = () =>{
setIsClicked(!isClicked);
if(!isClicked){disableScroll()}
else if(isClicked){enableScroll()}
}
const handleLogout = ()=>{
setIsLoggedIn(!isLoggedIn)
{<Redirect to={{
pathname: "/",
state: { from: props.location }
}}
/>}
console.log(isLoggedIn)
}
const getUser = ()=>{
Users.forEach((user)=>{
if(user.username === username)return user
})
}
const login=()=>{
const curUser = Users.filter((el)=>{
if(el.username === username) return el
})
if(curUser[0].username === username && curUser[0].pass.toString() === pass){
setAuthUser(curUser[0]);
setIsLoggedIn(!isLoggedIn)
enableScroll();
}else{alert('invalid username or password')}
}
return(
<LoginContext.Provider value={{isClicked,setIsClicked,handleLogin,setUsername,
setPass,login,isLoggedIn,authUser,setAuthUser,handleLogout}}>
<BrowserRouter>
<Switch>
<Route exact path='/' component={HomePage}>
<HomePage/>
</Route>
<ProtectedRoute exact path='/Dashboard' isAuth={isLoggedIn} component={Dashboard}/>
</Switch>
</BrowserRouter>
</LoginContext.Provider>
);
}
export default App;

React PrivateRoute is caught in a Route loop

I have a PrivateRoute component that protects any route requiring a valid login in the app. I have a route called Spec.tsx that is called from App.tsx. PrivateRoute will spit you out on the Login page if you're not logged in, which is great. And Login will send you directly to the Home page if you are logged in. Also great.
Right now, I'm caught in a loop where when I go to Spec, the app thinks I'm not logged in, and sends me to Login, which does think I'm logged in, and so sends me back to Home. currentUser is always calculated the same was, like const {currentUser} = useContext(AuthContext);
When I log the currentUser in PrivateRoute as I navigate to /spec/:id it says null once, and then gives the correct answer 3 more times. I suspect the null causes me to get booted to Login and then currentUser must be assigned correctly as I'm sent back to Home immediately. I don't even ever get to Spec.tsx, nothing I try to log there gets logged. Can anyone point out what I'm doing wrong? Thanks
//In App.tsx
<AuthProvider>
<Router history={history}>
<Navbar />
<Switch>
<PrivateRoute exact path="/" component={Home} />
<PrivateRoute exact path="/spec/:id" render={() => (
<Spec isEdit={true}/>
)}/>
<Route exact path="/login" component={Login} />
<Route exact path="/signup" component={Signup} />
</Switch>
</Router>
</AuthProvider>
//PrivateRoute.tsx
import React, { useContext } from "react";
import { Route, Redirect } from "react-router-dom";
import { AuthContext } from "../Util/Auth";
const PrivateRoute = ({ component: RouteComponent, ...rest }: any) => {
const {currentUser} = useContext(AuthContext);
console.log(currentUser)
return (
<Route
{...rest}
render={(routeProps: any) =>
!!currentUser ? (
<RouteComponent {...routeProps} />
) : (
<Redirect to={"/login"} />
)
}
/>
);
};
export default PrivateRoute
//Login.tsx
const Login = ({ history }: any) => {
const { currentUser } = useContext(AuthContext);
if (currentUser) {
return <Redirect to="/" />;
}
return (
//My JSX
)
My Auth provider is below, which handles all changes to currentUser
//Auth.tsx
import React, { useEffect, useState } from "react";
import { createNewUser } from "../Models/User";
import app from "./firebase";
const initialUser: any = null;
export const AuthContext = React.createContext(initialUser);
export const AuthProvider: React.FC = ({ children }) => {
const [currentUser, setCurrentUser] = useState<any>(null);
const [currentDBUser, setCurrentDBUser] = useState<any>(null);
const [pending, setPending] = useState<boolean>(true);
const [pending2, setPending2] = useState<boolean>(true);
useEffect(() => {
//Auth user
app.auth().onAuthStateChanged((user) => {
setPending(false)
setCurrentUser(user);
});
//Grab database data for user
async function fetchData() {
if (currentUser && !currentDBUser && pending2) {
const newUser: User = {fbUser: currentUser.uid, email: currentUser.email}
const userDBObject = await createNewUser(newUser);
if (userDBObject) {
setCurrentDBUser(userDBObject);
setPending2(false);
}
}
}
if (pending2) {
fetchData();
}
}, [currentUser, currentDBUser, pending2]);
if (pending) {
return <>Loading...</>
}
return (
<AuthContext.Provider value= {{ currentUser, currentDBUser }}>
{ children }
</AuthContext.Provider>
);
};

Resources