ReactJS Auth context lost when reloading page - reactjs

I'm new to ReactJS and am building a basic application. Here I'm using protected router and implementing an authorization mechanism with useContext and local storage. The aim is to redirect users that are not logged in to the Login Page if they attempt to access Dashboard.
After I do log in, the access token is saved to local storage and account info is saved in auth context. Then I go to Dashboard and I reload the page. I implemented a useEffect hook to check for token in local storage and I thought that when I reload at the Dashboard page, the auth provider would check for the token and return a result that I'm authenticated. However it doesn't work as expected so I am redirected to Login page (Although the useEffect callback was triggered)
Below is my code:
src\components\App\index.js
import { Routes, Route } from 'react-router-dom';
import Login from '../Login';
import Signup from '../Signup';
import GlobalStyles from '../GlobalStyles';
import ThemeProvider from 'react-bootstrap/ThemeProvider';
import RequireAuth from '../RequireAuth';
import Layout from '../Layout';
import Dashboard from '../Dashboard';
import Account from '../Account';
function App() {
return (
<GlobalStyles>
<ThemeProvider>
<Routes>
<Route path="/" element={<Login />} />
<Route path="/login" element={<Login />} />
<Route path="/signup" element={<Signup />} />
<Route element={<RequireAuth />}>
<Route path="/" element={<Layout />}>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/account" element={<Account />} />
</Route>
</Route>
</Routes>
</ThemeProvider>
</GlobalStyles>
);
}
export default App;
src\components\RequireAuth\index.js
import { useLocation, Navigate, Outlet } from 'react-router-dom';
import useAuth from '../../hooks/useAuth';
function RequireAuth() {
const { auth } = useAuth();
const location = useLocation();
return auth?.user ? (
<Outlet />
) : (
<Navigate to={{ pathname: '/', state: { from: location } }} replace />
);
}
export default RequireAuth;
src\hooks\useAuth.js
import { useContext } from 'react';
import { AuthContext } from '../context/AuthProvider';
function useAuth() {
return useContext(AuthContext);
}
export default useAuth;
src\context\AuthProvider.js
import { useEffect } from 'react';
import { createContext, useState } from 'react';
import api from '../helper/api';
const AuthContext = createContext({});
function AuthProvider({ children }) {
const [auth, setAuth] = useState({});
useEffect(() => {
const apiHelper = new api();
apiHelper.getAccountInfo().then((response) => {
setAuth(response.data);
});
}, []);
console.log(auth.user);
return (
<AuthContext.Provider value={{ auth, setAuth }}>
{children}
</AuthContext.Provider>
);
}
export { AuthContext, AuthProvider };
src\components\Login\index.js
import { useState, useRef, useEffect } from 'react';
import clsx from 'clsx';
import { Form, FormGroup, Button } from 'react-bootstrap';
import Alert from 'react-bootstrap/Alert';
import FloatingLabel from 'react-bootstrap/FloatingLabel';
import { useNavigate } from 'react-router';
import styles from './style.module.scss';
import logo from '../../assets/images/logo.png';
import LoadingSpinner from '../LoadingSpinner';
import api from '../../helper/api';
import useAuth from '../../hooks/useAuth';
const Login = () => {
const usernameRef = useRef();
const errorRef = useRef();
const [state, setState] = useState({
username: '',
password: ''
});
const { username, password } = state;
const [error, setError] = useState();
const [loading, setLoading] = useState(false);
useEffect(() => {
usernameRef.current.focus();
}, []);
const { setAuth } = useAuth();
const navigate = useNavigate();
const submitForm = (event) => {
event.preventDefault();
setLoading(true);
const apiHelper = new api();
apiHelper
.login({
username,
password
})
.then((response) => {
setLoading(false);
setError();
setAuth({ user: response.data.user });
localStorage.setItem('token', response.data.token);
navigate('/dashboard');
})
.catch((error) => {
setLoading(false);
setError(error.response.data.message);
usernameRef.current.focus();
});
};
return (
/** Some hmtml */
);
};
export default Login;
A video on how the error occurs: https://streamable.com/b1cp1t
Can anyone tell me where I'm wrong and how to fix it? Many thanks in advance!

you can think about a <Route> kind of like an if statement, if its path matches the current URL, it renders its element!
since the path of your first route in the list is "/" to the login page matches the need of the router it will redirect you there.
so, if you will delete this line:
<Route path="/" element={<Login />} />
and let the <RequireAuth /> take care of the path="/" it will check first if the user is logged in, if so let him continue.
if not it will redirect to "/login"

Related

React Routing with Authentication and Firebase User Contexts is not working

I have a problem with react context and react router. I have implemented following auth context:
import { useState, createContext, useContext, useEffect } from "react";
import { app} from "../firebase/config";
import { onAuthStateChanged } from "#firebase/auth";
import { getAuth } from "#firebase/auth";
import { Navigate
} from "react-router-dom";
export const FirebaseContext = createContext({
user: null,
isLoggedIn: false,
token: "",
});
export function FirebaseAuthProvider({ children }) {
const [user, setUser] = useState(null);
const [token, setToken] = useState(null);
console.log("In context");
useEffect(() => {
console.log("Before auth")
const auth = getAuth(app);
console.log("In use effect");
auth.onAuthStateChanged(async (user) => {
if (user) {
console.log("User", user);
setUser(user);
const token = await user.getIdToken();
setToken(token);
console.log("Token", token)
} else {
return <Navigate to="/login" />
}
});
console.log("End of useEffect");
}, []);
const contextValue = {
user: user,
isLoggedIn: !!token,
token: token,
};
return (
<FirebaseContext.Provider value={{contextValue}}>
{children}
</FirebaseContext.Provider>
);
}
And this is my app.js:
import "./App.css";
import { FirebaseAuthProvider, FirebaseContext } from "./context/firebaseContext";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import Layout from "./components/Layout/layout";
import HomePage from "./pages/homepage";
import NoPage from "./pages/404";
import SignInPage from "./pages/signin";
import ProtectedRoute from "./components/protectedRoute";
function App() {
return (
<FirebaseAuthProvider >
<BrowserRouter>
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<ProtectedRoute><HomePage /></ProtectedRoute>} />
{/* <Route path="blogs" element={<Blogs />} />*/}
<Route path="signin" element={<SignInPage />} />
<Route path="*" element={<NoPage />} />
</Route>
</Routes>
</BrowserRouter>
</FirebaseAuthProvider>
);
}
export default App;
Here is the protectedRoute.js:
import React, { useContext } from 'react'
import { FirebaseContext } from '../context/firebaseContext'
import { Navigate } from 'react-router-dom';
function ProtectedRoute({
redirectPath = '/signin',
children,
}) {
const {user, isLoggedIn} = useContext(FirebaseContext);
console.log("isLoged", isLoggedIn, user);
if (!isLoggedIn){
return <Navigate to={redirectPath} replace />;
}
return children;
}
export default ProtectedRoute
Here you can see the logs that are displayed:
The problem is that my function goes to Layout and ProtectedRoute components before useEffect hook in in React context, so it redirects the user to the signup page every time even if he already signed up. Does anyone has an idea what I am doing wrong?

why auth.currentUser is null on page refresh in Firebase Authetication

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

How do I useContext hook on React Router v6

I'm a beginner and I'm currently developing an application using a React Template. The template uses React Router v6 for the router with use Routes() hook.
Every route of the application is protected and can be only accessed by logging in
I'm planning to implement login using use Context() hook but I cant seem to figure out how to wrap the routes in the Provider tag.
My two doubts are:
How do I wrap my routes in the <Context Provider> tag
Should I wrap all my routes in an application like this.
First of all you will need the Context. I always prefer to write a hook:
import { createContext, useContext, useState } from "react";
const AuthContext = createContext({
isAuthenticated: false,
login: () => {},
logout: () => {}
});
export function AuthProvider({ children }){
const [isAuthenticated, setAuthenticated] = useState(false);
const login = () => {
setAuthenticated(true);
}
const logout = () => {
setAuthenticated(false);
}
return (
<AuthContext.Provider value={{isAuthenticated: isAuthenticated, login: login, logout: logout}}>
{children}
</AuthContext.Provider>
)
}
export default function AuthConsumer() {
return useContext(AuthContext);
}
Then you will need a private route component like this:
import React from 'react';
import { Navigate } from 'react-router-dom';
import useAuth from "./useAuth";
function RequireAuth({ children, redirectTo }) {
const { isAuthenticated } = useAuth();
return isAuthenticated ? children : <Navigate to={redirectTo} />;
}
export default RequireAuth;
Finally you will mix in your routes:
import React from 'react';
import { BrowserRouter, Route, Routes, Navigate } from 'react-router-dom';
import Login from './pages/Login';
import RequireAuth from './components/PrivateRoute';
import useAuth from "./components/useAuth";
const Home = () => <div><h1>Welcome home</h1></div>
const Dashboard = () => <h1>Dashboard (Private)</h1>;
function App() {
const { isAuthenticated } = useAuth();
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={
<RequireAuth redirectTo="/login">
<Dashboard />
</RequireAuth>
}>
</Route>
<Route path="/login" element={<Login />} />
<Route path='*' element={<Navigate to={isAuthenticated ? '/dashboard' : '/'} />} />
</Routes>
</BrowserRouter>
);
}
export default App;
I hope this will help.

Protected Routes with AWS Amplify using React context

I am migrating an app from Firebase to AWS Amplify. I want to create a React context which will provide route protection if the user is not logged in.
For example, my Auth.js file:
import React, { useEffect, useState, createContext } from 'react'
import fire from './firebase'
export const AuthContext = createContext()
export const AuthProvider = ({ children }) => {
const [currentUser, setCurrentUser] = useState(null)
useEffect(() => {
fire.auth().onAuthStateChanged(setCurrentUser)
}, [])
return (
<AuthContext.Provider value={{ currentUser }}>
{children}
</AuthContext.Provider>
)
}
And my App.js file:
import * as React from 'react'
import { BrowserRouter, Switch, Route } from 'react-router-dom'
import Navbar from './components/navbar/navbar'
import Home from './routes/Home'
import Register from './routes/Register'
import Footer from './components/footer/Footer'
import AlertProvider from './components/notification/NotificationProvider'
import MyAlert from './components/notification/Notification'
import { AuthProvider } from './Auth'
import PrivateRoute from './PrivateRoute'
const App = () => {
return (
<AuthProvider>
<BrowserRouter>
<AlertProvider>
<div className="app">
<Navbar />
<MyAlert />
<Switch>
<Route path="/" exact component={Home} />
<Route
path="/register"
exact
component={Register}
/>
<Route
path="/forgot-password"
render={(props) => <div>Forgot Password</div>}
/>
<Route path="*" exact={true} component={Home} />
</Switch>
<Footer />
</div>
</AlertProvider>
</BrowserRouter>
</AuthProvider>
)
}
export default App
This all works fine.
How would I do something similar with AWS Amplify? Essentially how would I create a Auth.js file that would wrap around my routes and give them a user context (which would update when the authentication status for the user is changed).
Thanks!
You can achieve this by setting up a custom protectedRoute HOC that will be used to protect any route that requires authentication. It will check if the user is signed-in and if the user is not signed-in then it will re-direct them to a specified route.
protectedRoute.js
import React, { useEffect } from 'react'
import { Auth } from 'aws-amplify'
const protectedRoute = (Comp, route = '/profile') => (props) => {
async function checkAuthState() {
try {
await Auth.currentAuthenticatedUser()
} catch (err) {
props.history.push(route)
}
}
useEffect(() => {
checkAuthState()
})
return <Comp {...props} />
}
export default protectedRoute
You can specify the default route or another route like the following:
// default redirect route
export default protectedRoute(Profile)
// custom redirect route
export default protectedRoute(Profile, '/sign-in')
You could also use the pre-built HOC from aws-amplify called withAuthenticator and that provides the UI as well as checking the users authentication status.
Sample use case for a profile page:
import React, { useState, useEffect } from 'react'
import { Button } from 'antd'
import { Auth } from 'aws-amplify'
import { withAuthenticator } from 'aws-amplify-react'
import Container from './Container'
function Profile() {
useEffect(() => {
checkUser()
}, [])
const [user, setUser] = useState({})
async function checkUser() {
try {
const data = await Auth.currentUserPoolUser()
const userInfo = { username: data.username, ...data.attributes, }
setUser(userInfo)
} catch (err) { console.log('error: ', err) }
}
function signOut() {
Auth.signOut()
.catch(err => console.log('error signing out: ', err))
}
return (
<Container>
<h1>Profile</h1>
<h2>Username: {user.username}</h2>
<h3>Email: {user.email}</h3>
<h4>Phone: {user.phone_number}</h4>
<Button onClick={signOut}>Sign Out</Button>
</Container>
);
}
export default withAuthenticator(Profile)
The routing for both would be the same and below I have linked a sample that I have used for both.:
import React, { useState, useEffect } from 'react'
import { HashRouter, Switch, Route } from 'react-router-dom'
import Nav from './Nav'
import Public from './Public'
import Profile from './Profile'
import Protected from './Protected'
const Router = () => {
const [current, setCurrent] = useState('home')
useEffect(() => {
setRoute()
window.addEventListener('hashchange', setRoute)
return () => window.removeEventListener('hashchange', setRoute)
}, [])
function setRoute() {
const location = window.location.href.split('/')
const pathname = location[location.length-1]
setCurrent(pathname ? pathname : 'home')
}
return (
<HashRouter>
<Nav current={current} />
<Switch>
<Route exact path="/" component={Public}/>
<Route exact path="/protected" component={Protected} />
<Route exact path="/profile" component={Profile}/>
<Route component={Public}/>
</Switch>
</HashRouter>
)
}
export default Router

How to use the useDispatch() hook inside the useEffect() hook?

I am using the useEffect() hook in my functional App component to check if the authentication has expired, so that I can dispatch an action to delete the persisted authentication state (using redux-persist). below is the code:
import React, { useEffect } from "react";
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
import { useDispatch, useSelector } from "react-redux";
import { signout } from "./app/state/authSlice";
import Signin from "./app/pages/Signin";
import Landing from "./app/pages/Landing";
const App = (props) => {
const dispatch = useDispatch();
const auth = useSelector((state) => state.auth);
const expired = new Date(Date.now()) >= new Date(auth.expires);
useEffect(() => {
const main = () => {
if (expired) {
console.log("Auth expired.");
dispatch(signout);
}
};
main();
}, [dispatch, expired]);
return (
<Router>
<Switch>
<Route exact path="/" {...props} component={Landing} />
<Route exact path="/signin" {...props} component={Signin} />
</Switch>
</Router>
);
};
export default App;
Now, I am getting the Auth expired console log when the expiry time is past, but the dispatch is not happening and my state still persists after the expiry time. I know that the signout action is correctly configured because I am using that in another component onClick.
This was just a typo. I forgot to call the signout() action creator.
Correct code below.
import React, { useEffect } from "react";
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
import { useDispatch, useSelector } from "react-redux";
import { signout } from "./app/state/authSlice";
import SigninPage from "./app/pages/Signin";
import LandingPage from "./app/pages/Landing";
const App = (props) => {
const dispatch = useDispatch();
const auth = useSelector((state) => state.auth);
const expired = new Date(Date.now()) >= new Date(auth.expires);
useEffect(() => {
const main = () => {
if (expired) {
console.log("Auth expired.");
dispatch(signout());
}
};
main();
}, [dispatch, expired]);
return (
<Router>
<Switch>
<Route exact path="/" {...props} component={LandingPage} />
<Route exact path="/signin" {...props} component={SigninPage} />
</Switch>
</Router>
);
};
export default App;

Resources