I am trying to authenticate user on each route change with react-router-dom and react hooks.
The idea is that each time user navigates to a route the system makes an api call and authenticate the user.
I need to achieve this because I use react-redux, and on each window reload the redux state is not persisted. So i need to set the isLoggedNow prop to true again:
const PrivateRoute = ({
component: Component,
checkOnEachRoute: checkReq,
isUserLogged,
...rest
}) => {
const [isLoggedNow, setLogged] = useState(isUserLogged);
useEffect(
() => {
const fetchStatus = async () => {
try {
await selectisUserLogged();
setLogged(true);
} catch (error) {
console.log(error);
}
};
fetchStatus();
},
[isUserLogged],
);
return (
<Route
{...rest}
render={props =>
isLoggedNow ? (
<div>
<Component {...props} />
</div>
) : (
<Redirect
to={{
pathname: '/login',
}}
/>
)
}
/>
);
};
I then would use the above PrivateRoute like this:
function App(props) {
return (
<div>
<Switch location={props.location}>
<Route exact path="/login" component={Login} />
<PrivateRoute exact path="/sidebar" component={Sidebar} />
</Switch>
</div>
);
}
First the isUserLogged is true, but after window reload I get an error Warning: Can't perform a React state update on an unmounted component.
So how can I achieve this, so on each window reload I authenticate the user? I am looking for some kind of componentWillMount.
Something like this works (where isUserLogged is a prop from redux):
function PrivateRoute({ component: Component, isUserLogged, ...rest }) {
const [isLoading, setLoading] = useState(true);
const [isAuthenticated, setAuth] = useState(false);
useEffect(() => {
const fetchLogged = async () => {
try {
setLoading(true);
const url = new URL(fetchUrl);
const fetchedUrl = await fetchApi(url);
setAuth(fetchedUrl.body.isAllowed);
setLoading(false);
} catch (error) {
setLoading(false);
}
};
fetchLogged();
}, []);
return (
<Route
{...rest}
render={props =>
// eslint-disable-next-line no-nested-ternary
isUserLogged || isAuthenticated ? (
<Component {...props} />
) : isLoading ? (
<Spin size="large" />
) : (
<Redirect
to={{
pathname: '/login',
}}
/>
)
}
/>
);
}
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 =>
....,
);
};
This is my app.js where all the routes
<Router>
<Routes>
<Route exact path="/" element={<Home />} />
<Route element={<PrivateRoute />}>
<Route exact path="/dashboard" element={<Dashboard />} />
<Route exact path="/payment" element={<Payment />} />]
</Route>
<Route exact path="/login" element={<Login />} />
</Routes>
</Router>
This is my PrivateRoute component
function PrivateRoute({ fetchMe, ...props }) {
const [type, setType] = useState("xxxxx");
const isAuthenticated = localStorage.getItem("authToken");
const navigate = useNavigate();
const [lodar, setLodar] = useState(false);
useEffect(() => {
setLodar(false);
if (isAuthenticated) {
(async () => {
const {
value: { user },
} = await fetchMe();
console.log({ data: user.step1 });
if (user.step === 1) {
navigate("/payment");
}
setLodar(false);
})();
}
}, []);
return (
<Spin indicator={antIcon} spinning={lodar}>
{isAuthenticated ? (
<>
<Header type={type} setType={setType} />
<Outlet context={[type, setType]} />
</>
) : (
<Navigate to="/login" />
)}
</Spin>
);
}
export default PrivateRoute;
So what I want to do here is to always redirect the user to the "/payment" after signup. and if the user again comes after login then it will again redirect it to the payment page so for that I am keeping a flag in my database user.step and checking by api call on the PrivateRoute component.
The issue is it loads the "/dashboard" page before the fetchUser api call which should not happen and show some lodar before. How can I do that?
Is there any better approach doing this since I always have to make an api call?
Kindly help!!!
Assuming <Spin indicator={antIcon} spinning={lodar}> is conditionally rendering either a loading spinner/indicator or the wrapped children then I think the issue is just the initial lodar state value. It doesn't appear the lodar state is ever toggled true.
I suggest starting with an initially true state so the component doesn't immediately render the Outlet or redirect when the component mounts, prior to any auth checks happening via the useEffect hook.
Example:
function PrivateRoute({ fetchMe, ...props }) {
const [type, setType] = useState("xxxxx");
const isAuthenticated = localStorage.getItem("authToken");
const navigate = useNavigate();
const [lodar, setLodar] = useState(true); // <-- initially true
useEffect(() => {
setLodar(true); // <-- toggle true when starting async logic
if (isAuthenticated) {
(async () => {
const {
value: { user },
} = await fetchMe();
console.log({ data: user.step1 });
if (user.step === 1) {
navigate("/payment");
}
setLodar(false); // <-- clear loading when complete
})();
}
}, []);
return (
<Spin indicator={antIcon} spinning={lodar}>
{isAuthenticated ? (
<>
<Header type={type} setType={setType} />
<Outlet context={[type, setType]} />
</>
) : (
<Navigate to="/login" />
)}
</Spin>
);
}
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")
);
I'm relatively new to React and Redux and learning them through my personal project.
The issue here is that isAuthed cannot use the updated Redux state after rest.dispatch(actions.isValidUser(json)) is executed. As far as I know, the Redux state is updated by the action. (But I don't see connect() is called after the update...I don't know if this is associated with this problem.)
Also I tried using Redux-thunk in my action file to fetch data from an API endpoint and using useEffect(), but it didn't solve the issue. Could you please help me out?
Thank you in advance.
**ProtedtedRoute.jsx**
import React from 'react';
import { Route, Redirect } from 'react-router-dom';
import { connect } from 'react-redux';
import * as actions from '../actions/actions';
function ProtectedRoute({ component: Component, isAuthed, ...rest }) {
async function verifyUser() {
// checking if a user is valid by checking JWT
const res = await fetch(ENDPOINT, reqOptions);
if (res.status === 200) {
const json = await res.json();
rest.dispatch(actions.isValidUser(json));
} else {
// failure handling
};
};
verifyUser();
return (
<Route
{...rest}
render={(props) => isAuthed == true ? <Component {...props} /> : <Redirect to={{ pathname: '/login', state: { from: props.location } }} />}
/>
);
};
export default connect(state => {
return {
isAuthed: state.isAuthenticated
}
})(ProtectedRoute);
**reducer.js**
const initState = {
data: {},
// when a user is valid, it will be ```true```
isAuthenticated: false
}
**App.js**
function App() {
return (
<Provider store={store}>
<BrowserRouter>
<div>
<div className="content">
<Switch>
<Route exact path="/" component={Home} />
<PublicRoute path="/login" component={LogIn} />
<PublicRoute path="/signup" component={SignUp} />
<ProtectedRoute path="/dashboard" component={Dashboard} />
</Switch>
...
**Login.jsx**
const res = await fetch(ENDPOINT, { reqOptions});
if (res.status === 200) {
props.history.push('/dashboard');
else{
// error handling
}
You don't want a function call like verifyUser(); just floating in the component. It needs to be inside a useEffect hook.
Your Login component fetches the endpoint before you redirect to Dashboard, so you should not need to fetch the endpoint again in order to access the Dashboard through PrivateRoute.
You can change your initialState to include isAuthenticated: undefined as in "we don't know if they are authenticate or not because we haven't checked yet."
Then in PrivateRoute, we only need to call verifyUser if the value of isAuthed is undefined meaning that we haven't checked yet. If it's true or false we just use that existing value.
We still have a bit of a problem with the aysnc flow because we don't want to to Redirect off of the PrivateRoute before verifyUser has finished. For that, we can conditionally render a loading state that shows while awaiting credentials.
I don't know that this is the most elegant solution but it should work
function ProtectedRoute({ component: Component, isAuthed, ...rest }) {
async function verifyUser() {
// checking if a user is valid by checking JWT
const res = await fetch(ENDPOINT, reqOptions);
if (res.status === 200) {
const json = await res.json();
rest.dispatch(actions.isValidUser(json));
} else {
// failure handling
}
}
useEffect(() => {
if (isAuthed === undefined) {
verifyUser();
}
}, [isAuthed]); //re-run when isAuthed changes
return (
<Route
{...rest}
render={(props) =>
isAuthed === undefined ? (
<Loading />
) : isAuthed === true ? (
<Component {...props} />
) : (
<Redirect
to={{ pathname: "/login", state: { from: props.location } }}
/>
)
}
/>
);
}
I have an isAuthenticated method that sends the access_token to the verification endpoint. if the response has an error I send the refresh_token to the refresh endpoint and whenever I get the new access token I save it in localStorage and ...
This method is async and I use it to decide to render a component or not.
const AuthenticatedRoute = ({ children, ...rest }) => {
const auth = useContext(AuthContext);
return (
<Route
{...rest}
**render={() => {
return auth.isAuthenticated() ? (**
<>{children}</>
) : (
<Redirect to="/login" />
);
}}
></Route>
);
};
I can't give an async method to the render method. I totally confused about how to handle this situation. maybe my strategy is wrong. can I somehow use isAuthenticated to decide to render a component in this strategy?
I think. Run async method in useEffect to change state
const AuthenticatedRoute = ({ children, ...rest }) => {
const auth = useContext(AuthContext);
const [isAuthenticated, setIsAuthenticated] = useState(false);
useEffect(() => {
setIsAuthenticated(auth.isAuthenticated())
},[auth])
return (
<Route
{...rest}
render={() => {
return isAuthenticated ? (
<>{children}</>
) : (
<Redirect to="/login" />
);
}}
></Route>
);
};