react-client-session Login loop - javascript

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

Related

I have a problem passing props from a state to the child

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.

How to prevent the current page from redirecting to another page when refreshing?

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;

React Router not rendering the right page when links are clicked, only works when I refresh the page

I'm doing a simple login system and log out for a testing blog that I'm trying to learn more about React. The links and redirections were working fine initially, but when I finished the Private Routes, the links started to render the Not Found page. It renders the Not Found page every time I click on any link, or it makes a redirection. And to render the right page, I need to refresh the page or type it manually.
Does anyone have any idea what's happening here that could help me to understand this problem better? I tried a lot of stuff, but nothing worked. I believe it's some problem in the App.js in the or the AuthContext.js that is a React Context that deals with the authentication part, but I still don't know what it is.
App.js
function App() {
function CustomRoute({ isPrivate, ...rest }) {
const { loading, authenticated } = useContext(Context);
if (loading) {
return <h1>Loading...</h1>;
}
if (isPrivate && !authenticated) {
return <Redirect to='/' />;
} else {
return <Route {...rest} />;
}
}
return (
<AuthProvider>
<Router history={history}>
<Fragment>
<Navbar />
<Switch>
<Route exact path='/' component={Login} />
<Route exact path='/register' component={Register} />
<CustomRoute isPrivate exact path='/posts' component={Posts} />
<CustomRoute
isPrivate
exact
path='/post-content'
component={PostContent}
/>
<CustomRoute isPrivate exact path='/new' component={NewPost} />
<CustomRoute isPrivate exact path='/edit' component={EditPost} />
<Route component={NotFound} />
</Switch>
</Fragment>
</Router>
</AuthProvider>
);
}
export default App;
AuthContext.js
const Context = createContext();
function AuthProvider({ children }) {
const [authenticated, setAuthenticated] = useState(false);
const [loading, setLoading] = useState(true);
useEffect(() => {
const token = localStorage.getItem('token');
if (token) {
axios.defaults.headers.Authorization = `Bearer ${JSON.parse(token)}`;
setAuthenticated(true);
}
setLoading(false);
}, []);
async function handleLogin(event, values) {
event.preventDefault();
const url = 'http://localhost:5000/auth/login';
const config = {
headers: {
'Content-Type': 'application/json',
},
};
await axios
.post(url, values, config)
.then((resp) => {
const token = resp.data.token;
localStorage.setItem('token', JSON.stringify(token));
axios.defaults.headers.Authorization = `Bearer ${token}`;
setAuthenticated(true);
history.push('/posts');
})
.catch((error) => {
if (error) {
setAuthenticated(false);
localStorage.removeItem('token');
axios.defaults.headers.Authorization = undefined;
history.push('/');
}
});
}
function handleLogout(event) {
event.preventDefault();
setAuthenticated(false);
localStorage.removeItem('token');
axios.defaults.headers.Authorization = undefined;
history.push('/');
}
return (
<Context.Provider
value={{ authenticated, handleLogin, handleLogout, loading }}>
{children}
</Context.Provider>
);
}
export { Context, AuthProvider };
Any ideas what could be causing it?
Thanks

Await API calls before render private route

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

How to perform authentication with React hooks and react-router

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',
}}
/>
)
}
/>
);
}

Categories