This is the Login component where I created the authentication state with the token as false, and after login convert as true
function Login() {
const [user, setUser] = useState({ name: '', email: '' });
const [error, setError] = useState('');
const [auth, setAuth] = useState({ token: false })
console.log(auth)
const Login = (details) => {
console.log(details);
if (
details.name === adminUser.name &&
details.email === adminUser.email &&
details.password === adminUser.password
) {
console.log('Logged in');
setAuth({
token: true,
})
This works perfectly fine, but now when I try to pass it to the PrivateRoute component, the auth is undefined
const PrivateRoutes = ({ auth }) => {
console.log(auth)
return (
auth ? <Outlet/> : <Navigate to="/login"/>
)
}
Also this is my App.jsx
function App() {
return (
<Routes>
<Route element={<PrivateRoutes />}>
<Route path="/*" element={<MainPage />} />
</Route>
<Route path="/login" element={<Login />} />
</Routes>
);
}
What do I need to change to get the data from the state and my guard functionality to work?
The auth state should be in a common ancestor so it can be passed down as props.
Example:
function App() {
const [auth, setAuth] = useState({ token: false });
return (
<Routes>
<Route element={<PrivateRoutes auth={auth} />}>
<Route path="/*" element={<MainPage />} />
</Route>
<Route path="/login" element={<Login setAuth={setAuth} />} />
</Routes>
);
}
const PrivateRoutes = ({ auth }) => {
return auth.token ? <Outlet/> : <Navigate to="/login" replace />;
}
function Login({ setAuth }) {
const navigate = useNavigate();
const [user, setUser] = useState({ name: '', email: '' });
const [error, setError] = useState('');
const login = (details) => {
if (
details.name === adminUser.name &&
details.email === adminUser.email &&
details.password === adminUser.password
) {
setAuth({ token: true });
navigate(/* new target path */, { replace: true });
...
}
...
So, to summarise, you want the state in the Login component to be able to access in PrivateRoutes component.
React recommends a pattern called lifting state up to achieve such similar use cases .
The idea is to have the auth state variable in common parent to both LoginPage component and PrivateRoute Component and pass it to both child components.
Something similar to the below image
enter image description here
So now changing auth (state) in any one of the components will effect the other component too. You should pass down setAuth() to the component where you want to modify the auth state.
In your example, Login component must be able to modify auth so that PrivateRoute component can use that state. You need to pass setAuth() to Login component and auth variable to PrivateRoute
Refer the docs here
The answer by Drew Reese makes it clear with the code example. Please refer it.
Related
function App() {
const [token, setToken] = useState();
useEffect(() => {
const auth = localStorage.getItem('auth_token');
setToken(auth);
}, [token]);
return (
<div>
<FullNavBar />
<Routes>
<Route path='/login' element={<Login />}></Route>
<Route path='/register' element={<Register />}></Route>
<Route path='/forgot_password' element={<ForgotPassword />}></Route>
<Route element={<ProtectedRoutes />}>
<Route path='/home' element={<Home />}></Route>
<Route path='/active_lottery' element={<Activelottery />}></Route>
</Route>
</Routes>
</div>
);
}
export default App;
Protected Routes:
import { Navigate, Outlet } from 'react-router';
const authorization = () => {
const token = localStorage.getItem('auth_token');
return token ? true : false;
};
const ProtectedRoutes = () => {
const isAuth = authorization();
return isAuth ? <Outlet /> : <Navigate to='/login' />;
};
export default ProtectedRoutes;
How can I make that navbar to render after changing the page?
After I'm trying to log in I'm redirected to the homepage but I still have the previous NavBar,is re-rendering after I'm refreshing the page.
Login redirect:
const onSubmitHandler = async (event) => {
event.preventDefault();
await axios
.post(
'url/dev/user/login',
loginForm,
)
.then((response) => {
localStorage.setItem(
'auth_token',
response.data.AuthenticationResult.AccessToken,
);
toast.success('Your in now');
setTimeout(() => {
navigate('/home');
}, 1000);
})
.catch((err) => {
console.log(err);
toast.error(err.response.data.message);
});
};
and in the fullNavBar component I want to update the pages from navBar.
export default function FullNavBar() {
const [menuOpen, setMenuOpen] = useState(false);
const [auth, setAuth] = useState(false);
const token = localStorage.getItem('auth_token');
useEffect(() => {
if (token) {
console.log(token);
setAuth(true);
}
}, [auth]);
return (
<div className="bg-gradient-to-r from-yellow-400 to-yellow-500">
<Navbar token={auth} menuOpen={menuOpen} setMenuOpen={setMenuOpen} />
{menuOpen && <MobileMenu>{navLinks(auth)}</MobileMenu>}
</div>
);
}
const navLinks = token => {
let pages = ['About', 'Contact', 'Login'];
if (token) {
pages = ['Buy', 'Tickets', 'History', 'Profile'];
}
return pages.map(page =>
page === 'Profile' ? (
<button key={page} className="inline-block">
{' '}
<img
className="rounded-full w-11 h-11"
src="https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQhumf_G7azRo-qCcnB533PPwZx386EK1cozzMAMEtW3A&s"
></img>
</button>
) : page === 'Tickets' ? (
<a
key={page}
className="no-underline text-gray-800 font-semibold hover:text-gray-500 relative bottom-4"
href={`${page.toLowerCase()}`}
>
100 {page}
</a>
) : (
<a
key={page}
className="no-underline text-gray-800 font-semibold hover:text-gray-500 relative bottom-4"
href={`${page.toLowerCase()}`}
>
{page}
</a>
),
);
};
everything is working fine after I'm refreshing the page but when it's redirected from login to home the navBar component isn't called
The issue here is that the code is trying to use localStorage actively as the source of truth. It is only to be used for longer-term storage of your React state. Use the token state in App as the active source of truth and pass the token state and state updater function down as props to the relevant components that care.
Example:
App
Use a state initializer function to set/provide the initial token state from localStorage. Use the useEffect hook to persist token state changes to localStorage.
function App() {
const [token, setToken] = useState(() => {
// Initialize from localStorage
return JSON.parse(localStorage.getItem('auth_token'));
});
useEffect(() => {
// Persist state changes to localStorage
localStorage.setItem('auth_token', JSON.stringify(token));
}, [token]);
return (
<div>
<FullNavBar token={token} /> // <-- pass token as prop
<Routes>
<Route
path='/login'
element={<Login setToken={setToken} />} // <-- pass state updater function
/>
<Route path='/register' element={<Register />} />
<Route path='/forgot_password' element={<ForgotPassword />} />
<Route
element={<ProtectedRoutes isAuth={!!token} />} // <-- pass isAuth prop
>
<Route path='/home' element={<Home />} />
<Route path='/active_lottery' element={<Activelottery />} />
</Route>
</Routes>
</div>
);
}
ProtectedRoutes
import { Navigate, Outlet } from 'react-router';
const ProtectedRoutes = ({ isAuth }) => {
return isAuth ? <Outlet /> : <Navigate to='/login' replace />;
};
export default ProtectedRoutes;
Login
Destructure and access the passed setToken callback and call it and pass the new token value to update the state in App.
const Login = ({ setToken }) = { // <-- access state updater function
...
const onSubmitHandler = async (event) => {
event.preventDefault();
await axios
.post('url/dev/user/login', loginForm)
.then((response) => {
setToken(response.data.AuthenticationResult.AccessToken); // <-- save token to state
toast.success('You're in now');
setTimeout(() => {
navigate('/home');
}, 1000);
})
.catch((err) => {
console.log(err);
toast.error(err.response.data.message);
});
};
...
};
FullNavBar
Access the passed token prop. Instead of using the token to set some local auth state just consume the token directly.
export default function FullNavBar({ token }) { // <-- access token prop
const [menuOpen, setMenuOpen] = useState(false);
return (
<div className="bg-gradient-to-r from-yellow-400 to-yellow-500">
<Navbar
token={token} // <-- pass to Navbar
menuOpen={menuOpen}
setMenuOpen={setMenuOpen}
/>
{menuOpen && (
<MobileMenu>
{navLinks(token)} // <-- pass to navLinks utility
</MobileMenu>
)}
</div>
);
}
const navLinks = token => {
let pages = ['About', 'Contact', 'Login'];
if (token) {
pages = ['Buy', 'Tickets', 'History', 'Profile'];
}
return pages.map(page =>
....,
);
};
I'm trying to implement Login Functionality to my app but whenever I do a FRESH Reload and try to redirect to my home page it enters an infinite loop, How could I go about resolving this?.
** Note **
I have refactored this in so many ways with and Without react state in place and many ways of redirecting but zero to no luck, It just loops infinitely in different ways.
My current method doesn't generate an error in the console its just the logs of 'mounting' and 'redi' you will read in my code infinitely looping till my CPU dies.
Code for reference
// Other Imports. lke axios, useEffect and components from bootstrap.
import { ReactSession } from 'react-client-session';
import { Redirect } from "react-router-dom";
const Login = () => {
const { handleSubmit, register } = useForm();
const BASE_URL = process.env.REACT_APP_BASE_URL
let [error, setError] = useState(false)
let [errorMessage, setErrorMessage] = useState('')
let [loginToken, setToken] = useState('')
const login = async (data, e ) => {
try { let loginData = {
email: data.email,
password: data.password,
}
let res = await axios.post(BASE_URL + '/users/admin/login', loginData)
if (res.status === 200) {
ReactSession.set('token', res.data.token)
// return <Redirect to="/admin/index" />
setToken(res.data.token)
}} catch (error) {
setError(error)
setErrorMessage("There's been a problem while you were loggin in")
}
}
useEffect(() => {
const token = ReactSession.get("token");
setToken(token)
console.log('mounting')
}, [loginToken])
if (loginToken){
console.log('redi')
return <Redirect to="/admin/meals" />
}
return (
// Login Form components.
A copy of my router for reference
// More Imports
import AdminLayout from "layouts/Admin.js";
import AuthLayout from "layouts/Auth.js";
ReactSession.setStoreType("localStorage");
const token = ReactSession.get("token");
ReactDOM.render(
<BrowserRouter>
<Switch>
<Route path="/auth" render={(props) => <AuthLayout {...props} />} />
<Route path="/admin" render={(props) =>
token
? <AdminLayout {...props} />
: <Redirect from="*" to="/auth/login" />
}
/>
{(token)
? <Redirect from="/" to="/admin/index" />
: <Redirect from="*" to="/auth/login" />
}
</Switch>
</BrowserRouter>,
document.getElementById("root")
);
Every time I'll reload the Profile page, it will redirect itself to the Homepage. How can I fix this?
Once a user has successfully logged in, he or she will be directed to the Homepage. On the Homepage, there's a Profile Page. I can successfully load the Profile page, however, once I'll reload this, the user will be redirected to the homepage again.
//custom hook
export function useAuth() {
const [currentUser, setCurrentUser] = useState();
useEffect(() => {
const unsub = onAuthStateChanged(auth, (user) => setCurrentUser(user));
return unsub;
}, []);
return currentUser;
}
App.js
function App() {
const currentUser = useAuth();
const user = auth.currentUser;
const navigate = useNavigate();
useEffect(() => {
const unsub = onAuthStateChanged(auth, (user) => {
if (user) {
// User is signed in, see docs for a list of available properties
// https://firebase.google.com/docs/reference/js/firebase.User
const uid = user.uid;
console.log(uid);
navigate("/Homepage");
// ...
} else {
// User is signed out
// ...
navigate("/");
}
});
return unsub;
}, []);
return (
<div>
<div>
<Routes>
{currentUser ? (
<>
<Route
path="/"
element={
<MainLayout>
<LoginPage />
</MainLayout>
}
/>
<Route path="/Homepage" element={<Home />} />
<Route path="/Profile" element={<ProfilePage />} />
</>
) : (
<>
<Route
path="/"
element={
<MainLayout>
<LoginPage />
</MainLayout>
}
/>
</>
)}
</Routes>
</div>
</div>
);
}
export default App;
If I'll console.log(currentUser) this is what it shows:
Also in: https://www.reddit.com/r/reactjs/comments/smfsro/how_to_prevent_the_page_from_redirecting_to/
With Protected Route:
{currentUser ? (
<>
<Route
path="/"
element={
<MainLayout>
<LoginPage />
</MainLayout>
}
/>
<Route path="/Homepage" element={<Home />} />
<Route
path="/Profile"
element={
<PrivateRoute>
<ProfilePage />
</PrivateRoute>
}
/>
</>
) : (
<>
<Route
path="/"
element={
<MainLayout>
<LoginPage />
</MainLayout>
}
/>
</>
)}
PrivateRoute
import React from "react";
import { Navigate, Outlet, useLocation } from "react-router-dom";
import { useAuth } from "../../Firebase/utils";
const PrivateRoute = () => {
const currentUser = useAuth();
// // const !currentUser = null; // determine if authorized, from context or however you're doing it
// // If authorized, return an outlet that will render child elements
// // If not, return element that will navigate to login page
// return currentUser ? <Outlet /> : <Navigate to="/" />;
let location = useLocation();
if (!currentUser) {
console.log(currentUser);
return <Navigate to="/" state={{ from: location }} replace />;
}
};
export default PrivateRoute;
I believe the issue is that currentUser doesn't have a default value, so it's always undefined when you first load the page. You should add a loading state to make sure the user's status is checked before using currentUser to do any logic.
Note: I haven't run any of the code below so there might be some mistakes, but it's just a general idea for reference.
export function useAuth() {
const [isLoading, setIsLoading] = useState(true); // checking the user's status
const [currentUser, setCurrentUser] = useState();
useEffect(() => {
const unsub = onAuthStateChanged(auth, (user) => {
setCurrentUser(user)
setIsLoading(false) // finished checking
});
return unsub;
}, []);
return {currentUser, isLoading};
}
So when you're checking if the user is logged in:
if(!isLoading && currentUser){
// is finished loading and user is logged in
}
I also recommend you store currentUser in a context instead of using a custom hook, since custom hooks won't allow you to share the state between components and it will be mounted every time you use it.
Instead your hook can be used to get the value from the context.
Something like this:
// store currentUser inside context
const AuthContext = createContext();
export const AuthProvider = ({children}) => {
const [isLoading, setIsLoading] = useState(true);
const [currentUser, setCurrentUser] = useState();
useEffect(() => {
const unsub = onAuthStateChanged(auth, (user) => {
setCurrentUser(user)
setIsLoading(false)
});
return unsub;
}, []);
return (
<AuthContext.Provider value={{currentUser, isLoading}}>
{children}
</AuthContext.Provider>
)
}
// read context value with custom hook
export useAuth = () => useContext(AuthContext)
It'll still be the same when you're using useAuth, only that it's getting states from the context instead.
const {currentUser, isLoading} = useAuth()
---Edit to question---
Context is used to share data throughout the application, it's great when you need to use the same data in multiple places. You can store data and pass it down with a provider, and access it with a consumer. I won't go into the details, but you can read more about Context API here: https://reactjs.org/docs/context.html#gatsby-focus-wrapper
About how to use AuthProvider, normally you could wrap it like this in App.js
// App.js
<AuthProvider>
<div>
<Routes>{/* ... */}</Routes>
</div>
</AuthProvider>
But in this case since you're using currentUser in App.js already, you'd have to update AuthProvider to HOC, and wrap it around your component
// AuthContext
export const withAuthProvider = WrappedComponent => ({children}) => {
// ... the rest is the same
}
// App.js
export default withAuthProvider(App);
The reason for this is because context values are only accessible within the scope the provider is wrapped around. Without HOC, your provider is wrapped around your routes, which means the value is not accessible within App.js itself, but is accessible in MainLayout, LoginPage etc.. With HOC, however, since you've wrapped the entire App.js, it can access the values in AuthContext.
You could just use AuthContext to pass parent to child component like below:
import React from 'react'
import { createContext, useState } from "react";
const AuthContext = createContext({});
export const AuthProvider = ({ children }) => {
const [auth, setAuth] = useState({});
return (
<AuthContext.Provider value={{ auth, setAuth }}>
{children}
</AuthContext.Provider>
)
}
export default AuthContext;
I have protected routes that passing auth enabled true or false to allow users access auth page. Every page load, it calls the auth token API and check for valid. If token invalid, redirect to login page.
ProtectedRoute.js
const ProtectedRoute = ({isEnabled, ...props}) => {
return (isEnabled) ? <Route {...props} /> : <Redirect to="/login"/>;
};
export default ProtectedRoute;
Routes.js
import {withRouter, Switch, Route } from "react-router-dom";
export default withRouter(({ location }) => {
const [isAuth, setIsAuth] = useState(false)
useLayoutEffect(() => {
(async() => {
if(accessToken){
let res = await ValidateLoginToken(accessToken)
if (res && res.data && res.data.status === 200){
setIsAuth(res.data.valid)
} else setIsAuth(false);
} else setIsAuth(false)
})()
},[isAuth])
return (
<Switch>
<ProtectedRoute path="/dashboard" component={Dashboard} isEnabled={isAuth} />
<Route path="/" component={Login} />
</Switch>
)
}
App.js
const history = createBrowserHistory();
function App() {
return (
<Router history={history}>
<Routes/>
</Router>
)
}
export default App;
Dashboard.js
export const Dashboard = () => {
return (
<div class="ui form-user center raised padded segment">
<a href="/section1">
<div id="section1" class="ui floated right basic red button">
Directory #1
</div>
</a>
</div>
)
}
Problem
When auth users browse authenticated page (Dashboard), redirects to login page. The reason is that verify token API returns after ProtectedRoute rendered as isAuth equals to false.
You can use another state variable to wait for api execution before initializing routes.
const [isAuth, setIsAuth] = useState(false)
const [checked, setChecked] = useState(false)
useLayoutEffect(() => {
(async() => {
if(accessToken){
let res = await ValidateLoginToken(accessToken)
if (res && res.data && res.data.status === 200){
setIsAuth(res.data.valid)
} else setIsAuth(false);
} else setIsAuth(false)
setChecked(true)
})()
},[isAuth])
Then in Routes, you can do something like:
<Switch>
{
!checked?(<React.Fragment/>):!isAuth?(<Route path="/" component={Login} />):(<ProtectedRoute path="/dashboard" component={Dashboard} loginState={loginState} />)
}
</Switch>
I usually separate session routes and no session routes in separate hooks and it works fine.
Instead of isAuth being a boolean, you use a string or number to increase the number of states. For example rename isAuth to loginState which can be "pending", "authenticated" or "unauthenticated". Then use "pending" as the initial state and add an additional scenario. You could for example return null to render nothing, render a spinning circle, etc.
Here is an example rendering null (nothing) while the login token is being authenticated:
ProtectedRoute.js
const loginRoutes = {
pending: ( ) => null,
authenticated: (...props) => <Route {...props} />,
unauthenticated: ( ) => <Redirect to="/login"/>,
};
const ProtectedRoute = ({loginState = "pending", ...props}) => {
const LoginRoute = loginRoutes[loginState];
return <LoginRoute {...props} />;
};
export default ProtectedRoute;
Route.js
import { withRouter, Switch, Route } from "react-router-dom";
export default withRouter(({ location }) => {
const [loginState, setLoginState] = useState("pending")
useLayoutEffect(() => {
(async() => {
if(accessToken){
let res = await ValidateLoginToken(accessToken)
if (res && res.data && res.data.status === 200) {
setLoginState(res.data.valid ? "authenticate" : "unauthenticated")
} else setLoginState("unauthenticated");
} else setLoginState("unauthenticated")
})()
}, [loginState])
return (
<Switch>
<ProtectedRoute path="/dashboard" component={Dashboard} loginState={loginState} />
<Route path="/" component={Login} />
</Switch>
)
}
As a disclaimer, I have no experience with React Router so I've kept the example as close to the original as possible.
You could use the initial state as null to differentiate the different states of your component.
null -> API token no called yet.
true -> token verified
false -> token verification failed.
export default function ProtectedRoute(props) {
const [isAuth, setIsAuth] = React.useState(false)
const history = useHistory();
React.useEffect(() => {
async function validateToken() {
if(accessToken){
let res = await ValidateLoginToken(accessToken)
if (res && res.data && res.data.status === 200){
setIsAuth(true)
return;
}
}
history.push('/login')
}
validateToken();
}, [isAuth]);
if (isAuth === null) return null;
return props.children;
}
export default function App() {
return (
<Router>
<Switch>
<Route path="/" exact component={HomePage} />
<Route exact path="/login" component={Login} />
<ProtectedRoute>
<Route path="/dashboard" component={Dashboard} />
</ProtectedRoute>
</Switch>
</Router>
)
}
I have a react + firebase application which has protected routes. I face issue if a logged in user accesses the login page. The issue is that the login page gets displayed for a second and then redirects to the home page i.e protected route. I feel the issue is because the value retrieved from context in the login page to check if user is authenticated gets updated after the route is resolved. Can someone give me pointers on how should I fix this. Ideally I would not want the user to see the login page for sometime if the user is already authenticated.
//App.js
render() {
return (
<AuthProvider>
<Router>
<Switch>
<PrivateRoute exact path="/" component={Home}></PrivateRoute>
<Route exact path="/login" component={LoginPage}></Route>
<Route exact path="/signup" component={SignUp}></Route>
</Switch>
</Router>
</AuthProvider>
);
}
}
//AuthProvider
import React, { useEffect, useState } from "react"
import { fire } from "./Fire"
export const AuthContext = React.createContext();
//this component will maintain the current user throughout the app
export const AuthProvider = ({ children }) => {
const [currentUser, setCurrentUser] = useState(null)
//empty array as second arg to useEffect hook as we only want to trigger it once
useEffect(() => {
console.log("useEffect")
fire.auth().onAuthStateChanged(setCurrentUser)
}, [])
return (
<AuthContext.Provider value={{ currentUser }}>{children}</AuthContext.Provider>
)
}
//PrivateRoute
const PrivateRoute = ({ component: RouteComponent, ...rest }) => {
//useContext hook makes it very easy to retrieve the value
const { currentUser } = useContext(AuthContext)
return (
<Route {...rest} render={
routeProps => {
console.log("currentUser" + currentUser);
return !!currentUser ? (
<RouteComponent {...routeProps} />
) : (
<Redirect to={"/login"} />
)
}
} />
)
}
//login
render() {
if (this.props.context.currentUser)
return <Redirect to="/" />
return (
<Login email={this.state.email} password={this.state.password} inputHandler={this.onInputChange} loginHandler={this.onLoginClick} />
)
}
You should probably add a loading check in your PrivateRoute file which will just show a Loading... or a loader if the user is not loaded yet.
For that you will have to do some minor changes in your AuthProvider file and in PrivateRoute.
//PrivateRoute
const PrivateRoute = ({ component: RouteComponent, ...rest }) => {
//useContext hook makes it very easy to retrieve the value
const { currentUser } = useContext(AuthContext)
return (
<Route {...rest} render={
routeProps => {
console.log("currentUser" + currentUser);
return !!currentUser ? (
<RouteComponent {...routeProps} />
) : currentUser === 'loading' ? <h1>Loading...</h1>
:(
<Redirect to={"/login"} />
)
}
} />
)
}