React PrivateRoute is caught in a Route loop - reactjs

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>
);
};

Related

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

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 }} />;
}

Login redirect rendering twice

I'm using reach router for my routes. I was able to protect the dashboard and expose the login page and redirect if the user is not logged in but now if I enter a url it will do a quick redirecto to login and then to home instead of the page actually entered.
I noticed because of the useEffect to fetch the user the component renders twice: 1 without user (redirects to login) the other one with user and redirects to home.
Routes file
const AdminRoutes = () => {
return (
<Router>
<MainLayout path="/admin">
<HomePage path="/" />
<CarTransfer path="/cartransfers">
<CarTranserList path="/list" />
<CarTransferCreate path="/new" />
</CarTransfer>
<User path="/users">
<UserList path="/list" />
<UserCreate path="/new" />
</User>
</MainLayout>
<LoginPage path="/login" />
</Router>
);
};
Layout file
import { useState, useEffect } from "react";
import { Redirect, useNavigate } from "#reach/router";
import { Layout } from "antd";
import SiderMenu from "./SiderMenu";
import LayoutBanner from "./LayoutBanner";
import { useSelector, useDispatch } from "react-redux";
import {
userSelector,
fetchUserBytoken,
clearState,
} from "../../features/authSlice";
const { Content } = Layout;
const MainLayout = ({ children }) => {
const user = useSelector(userSelector);
const [collapsed, setCollapsed] = useState(false);
const navigate = useNavigate();
const dispatch = useDispatch();
const { isFetching, isError } = useSelector(userSelector);
useEffect(() => {
dispatch(
fetchUserBytoken({
token: localStorage.getItem("token"),
id: localStorage.getItem("id"),
})
);
}, []);
useEffect(() => {
if (isError) {
dispatch(clearState());
navigate("/login");
}
}, [isError]);
const handleOnCollapse = () => {
setCollapsed((prevState) => !prevState);
};
if (isFetching) {
return <div>Loading</div>;
}
if (user.id === "") {
return <Redirect noThrow to="/login" />;
}
return (
<Layout>
<SiderMenu collapsed={collapsed} handleOnCollapse={handleOnCollapse} />
<Layout>
<LayoutBanner
collapsed={collapsed}
handleOnCollapse={handleOnCollapse}
/>
<Content>{children}</Content>
</Layout>
</Layout>
);
};
export default MainLayout;
The second question would be how to get to the same page you were before the login redirect after login.
Thanks

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;

I can't access a private route using firebase

I am creating a private route to be accessed only when the firebase listener verifies that the user is logged in, but I cannot access this route the way I am doing.
const PrivateRoute = ({ component: Component, ...rest }) => {
let autenticado = false;
firebase.auth().onAuthStateChanged((user) => {
if (user) {
autenticado = true;
} else {
autenticado = false;
}
});
return (
<Route
{...rest}
render={(props) =>
autenticado ? (
<Component {...props} />
) : (
<Redirect to={{ pathname: "/", state: { from: props.location } }} />
)
}
/>
);
};
const Routes = () => (
<BrowserRouter>
<Switch>
<Route path="/" exact component={Login} />
<PrivateRoute path="/home" component={Home} />
</Switch>
</BrowserRouter>
);
export default Routes;
Alvaro's solution is good, but Robert Terrell brought up a good point:
if you refresh your app, the private route kicks you back to the login page because it takes a few moments for the currentUser to re register
.
To fix this, id wait for the authcontext to load before displaying the site on line 24 in AuthContext.tsx:
import React, { useEffect, useState } from "react";
import {auth, firebase} from "../firebaseSetup";
type ContextProps = {
user: firebase.User | null;
};
export const AuthContext = React.createContext<Partial<ContextProps>>({});
export const AuthProvider = ({ children }: any) => {
const [user, setUser] = useState(null as firebase.User | null);
const [loading, setLoading] = useState(true);
useEffect(() => {
auth.onAuthStateChanged((user: any) => {
setUser(user);
setLoading(false);
});
}, []);
return (
<AuthContext.Provider
value={{user}}>
{!loading && children}
</AuthContext.Provider>
);
}
It's hard to tell where your code is going wrong without seeing other components. I do basically the same approach when I set up firebase auth and set up private routes. I bring my context into my PrivateRoute component like so:
import React, { useContext } from "react";
import { Route, Redirect } from "react-router-dom";
import { AuthContext } from "./Auth";
const PrivateRoute = ({ component: RouteComponent, ...rest }) => {
const {currentUser} = useContext(AuthContext);
return (
<Route
{...rest}
render={routeProps =>
!!currentUser ? (
<RouteComponent {...routeProps} />
) : (
<Redirect to={"/admin/login"} />
)
}
/>
);
};
My Auth.js file is where I create my context like so:
import React, { useEffect, useState } from "react";
import app from "./base.js";
export const AuthContext = React.createContext();
export const AuthProvider = ({ children }) => {
const [currentUser, setCurrentUser] = useState(null);
useEffect(() => {
app.auth().onAuthStateChanged(setCurrentUser);
}, []);
return (
<AuthContext.Provider
value={{
currentUser
}}
>
{children}
</AuthContext.Provider>
);
};
I initiate my firebase instance in a file called base.js that looks like so :
import * as firebase from "firebase/app";
import "firebase/auth";
console.log(process.env.REACT_APP_FIREBASE_DATABASE)
const app = firebase.initializeApp({
apiKey: process.env.REACT_APP_FIREBASE_KEY,
authDomain: process.env.REACT_APP_FIREBASE_DOMAIN,
databaseURL: process.env.REACT_APP_FIREBASE_DATABASE,
projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID,
storageBucket: process.env.REACT_APP_FIREBASE_STORAGE_BUCKET,
messagingSenderId: process.env.REACT_APP_FIREBASE_SENDER_ID
});
export default app;
In your routes you would then add the AuthProvider
const Routes = () => (
<BrowserRouter>
<Switch>
<AuthProvider>
<Route path="/" exact component={Login} />
<PrivateRoute path="/home" component={Home} />
</AuthProvider>
</Switch>
</BrowserRouter>
);
export default Routes;

React Router - Login redirect to a private route goes to landing page instead

I have authentication up and running, but I am trying to get a redirect to the (private route) application page after a successful sign in. Instead a user is redirected to the landing page, in which they have to navigate to the application page manually.
My route is wrapped as follows
<Router>
<div className="app">
<Route exact path="/" component={Landing} />
<PrivateRoute exact path="/app" component={AppHome} />
<Route exact path="/login" component={Login} />
<Route exact path="/signup" component={SignUp} />
</div>
</Router>
Where the login component checks and redirects via
const { currentUser } = useContext(AuthContext);
if (currentUser) {
console.log(currentUser)
return <Redirect to="/app" />
}
Full login component
import React, { useCallback, useContext } from 'react'
import { BrowserRouter as Router, Link } from 'react-router-dom'
import { withRouter, Redirect } from 'react-router'
import FBase from '../firebase.js'
import { AuthContext } from '../auth/Auth'
const Login = ({ history }) => {
const handleLogin = useCallback(
async event => {
event.preventDefault()
const { email, password } = event.target.elements;
try {
await FBase
.auth()
.signInWithEmailAndPassword(email.value, password.value);
history.push('/')
} catch (error) {
alert(error)
}
},
[history]
);
const { currentUser } = useContext(AuthContext);
if (currentUser) {
console.log(currentUser)
return <Redirect to="/app" />
}
return (
///UI - Form calls handleLogin
)
}
export default withRouter(Login)
Console is clearly logging the user after a successful sign in, so this may be a lifecycle issue.
The private route itself (the redirect in here works)
const PrivateRoute = ({ component: RouteComponent, ...rest }) => {
const { currentUser } = useContext(AuthContext)
return (
<Route
{...rest}
render = {routeProps => !!currentUser ? (
<RouteComponent {...routeProps} />
) : (
<Redirect to={"/login"} />
)
}
/>
)
}
After further testing, it seems the redirect just doesn't work at all, as I've tried redirecting to signup too.
You could use useHistory hook to handle redirection, it's easier and you can directly implement it in your async/await function.
import {useHistory} from "react-router-dom"
const history = useHistory()
async function foo(){
const res = await stuff
history.push("/app")
}
Also, don't write exact path everywhere in your route. This is only necessary for the main root "/".

Resources