Condition based routing in react - reactjs

I want to render routes based on the user login status.So, I made a function to check if the user is authenticated on the backend using axios and call that function in useEffect in App component and store the respone in the useState. And then I use condition in the Route component's element. If the user is authenticated, redirect the user to home page. If not, redirect to login page. But the problem is when I try to enter the route from url bar, I always get redirected to login page even I'm authenticated.
these are my codes(I removed some unrelated codes to look cleaner)
function App() {
const navigate = useNavigate();
const [isLoggedIn, setIsLoggedIn] = useState("NOT_LOGGED_IN");
const [user, setUser] = useState({});
const verifyLogin = async () => {
const res = await axios({
url: "http://localhost:5000/isloggedin",
method: "get",
withCredentials: true
})
if (res.data.isLoggedIn && isLoggedIn === "NOT_LOGGED_IN") {
setIsLoggedIn("LOGGED_IN");
setUser(res.data.user)
} else if (!res.data.isLoggedIn && isLoggedIn === "LOGGED_IN") {
setIsLoggedIn("NOT_LOGGED_IN");
setUser({})
}
}
useEffect(() => {
verifyLogin()
}, [])
return (
<div className="App">
<Routes>
<Route path='/' element={isLoggedIn === "LOGGED_IN" ? <Home isLoggedIn={isLoggedIn} user={user} /> : <Navigate to="/login" />} exact />
<Route path='/register' element={<Register handleRegister={handleRegister} registerError={registerError} />} />
<Route path='/register/:userId/info' element={isLoggedIn === "LOGGED_IN" ? <RegisterInfo handleRegister={handleRegister} registerError={registerError} /> : <Navigate to={"/register"} />} />
<Route path='/login' element={<Login isLoggedIn={isLoggedIn} handleLogin={handleLogin} logout={logout} />} />
</Routes>
</div >
);
}
I'm sorry if my writing made you confused. I'm not so good at English.

Issue
The issue is that your initial isLoggedIn state matches your unauthenticated state. When the app initially loads and you are trying to access any route the app uses this "NOT_LOGGED_IN" initial isLoggedIn state value and determines the user is not logged in and redirects accordingly.
Solution
The common solution is to start from an "indeterminant" state that is neither authenticated nor unauthenticated and conditionally render nothing, or a loading indicator, etc... anything but the routed component or the redirect.
Example:
function App() {
const navigate = useNavigate();
const [isLoggedIn, setIsLoggedIn] = useState(); // <-- initially undefined
const [user, setUser] = useState({});
useEffect(() => {
const verifyLogin = async () => {
const res = await axios({
url: "http://localhost:5000/isloggedin",
method: "get",
withCredentials: true
});
if (res.data.isLoggedIn && isLoggedIn === "NOT_LOGGED_IN") {
setIsLoggedIn("LOGGED_IN");
setUser(res.data.user);
} else if (!res.data.isLoggedIn && isLoggedIn === "LOGGED_IN") {
setIsLoggedIn("NOT_LOGGED_IN");
setUser({});
}
};
verifyLogin();
}, []);
if (isLoggedIn === undefined) { // <-- check if undefined
return null; // or loading indicator, etc...
}
return (
<div className="App">
<Routes>
<Route
path='/'
element={isLoggedIn === "LOGGED_IN"
? <Home isLoggedIn={isLoggedIn} user={user} />
: <Navigate to="/login" />
}
/>
<Route
path='/register'
element={(
<Register
handleRegister={handleRegister}
registerError={registerError}
/>
)}
/>
<Route
path='/register/:userId/info'
element={isLoggedIn === "LOGGED_IN"
? (
<RegisterInfo
handleRegister={handleRegister}
registerError={registerError}
/>
)
: <Navigate to={"/register"} />
}
/>
<Route
path='/login'
element={(
<Login
isLoggedIn={isLoggedIn}
handleLogin={handleLogin}
logout={logout}
/>
)}
/>
</Routes>
</div >
);
}
Further Suggestions
Abstract the isLoggedIn state and auth verification into a React context.
Abstract the protected routing logic into a wrapper or layout component.
This makes your code quite a bit more DRY.

Related

Is there a way to disable <Navigate> when I refresh the page? [duplicate]

I'm using Firebase v9 and react-router v6. I haven't used v6 so this was quite confusing. How can I make it where the guest user can only access the login page. Only users who were logged in can access the homepage and other pages.
Everytime I'll reload any page, it will show this in the console but it will still direct the user to the right page :
No routes matched location "/location of the page"
How can I use a private route for the profile page?
//custom hook
export function useAuth() {
const [currentUser, setCurrentUser] = useState();
useEffect(() => {
const unsub = onAuthStateChanged(auth, (user) => setCurrentUser(user));
return unsub;
}, []);
return currentUser;
}
App.js
import { auth, useAuth } from "./Firebase/utils";
import { onAuthStateChanged } from "firebase/auth";
function App() {
const currentUser = useAuth();
const user = auth.currentUser;
const navigate = useNavigate();
console.log(currentUser?.email);
useEffect(() => {
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("/Home");
// ...
} else {
// User is signed out
// ...
navigate("/");
}
});
}, []);
return (
<div>
<div>
<Routes>
{currentUser ? (
<>
//If i do it this way and I'll go the profile page and reload it, it will always go to back to the Homepage.
<Route path="/Home" element={<Home />} />
<Route path="/Profile" element={<ProfilePage />} />
</>
) : (
<>
<Route
path="/"
element={
<LogInPage />
}
/>
</>
)}
</Routes>
</div>
</div>
);
}
export default App;
This is what the console.log(user) shows:
Package.json file:
Issues
The main issue is that the currentUser value is initially falsey
const [currentUser, setCurrentUser] = useState();
and you are making a navigation decision on unconfirmed authentication status in App
<Routes>
{currentUser ? (
<>
// If i do it this way and I'll go the profile page and reload it,
// it will always go to back to the Homepage.
<Route path="/Home" element={<Home />} />
<Route path="/Profile" element={<ProfilePage />} />
</>
) : (
<>
<Route
path="/"
element={<LogInPage />}
/>
</>
)}
</Routes>
When refreshing the page the currentUser state is reset, is undefined, i.e. falsey, and only the "/" path is rendered.
Solution
In react-router-dom is a common practice to abstract route protection into a specialized "protected route" component. You will also want to conditionally handle the indeterminant state until your Firebase auth check has had a chance to confirm an authentication status and update the currentUser state.
Example:
export function useAuth() {
const [currentUser, setCurrentUser] = useState(); // <-- initially undefined
useEffect(() => {
const unsub = onAuthStateChanged(auth, (user) => setCurrentUser(user)); // <-- null or user object
return unsub;
}, []);
return { currentUser };
}
AuthWrapper - Uses the useAuth hook to check the authentication status of user. If currentUser is undefined it conditionally returns early null or some other loading indicator. Once the currentUser state is populated/defined the component conditionally renders either an Outlet for nested/wrapped Route components you want to protect, or the Navigate component to redirect to your auth route.
import { Navigate, Outlet, useLocation } from 'react-router-dom';
const AuthWrapper = () => {
const location = useLocation();
const { currentUser } = useAuth();
if (currentUser === undefined) return null; // <-- or loading spinner, etc...
return currentUser
? <Outlet />
: <Navigate to="/" replace state={{ from: location }} />;
};
App - Unconditionally renders all routes, wrapping the Home and Profile routes in the AuthWrapper layout route.
function App() {
return (
<div>
<div>
<Routes>
<Route element={<AuthWrapper />}>
<Route path="/Home" element={<Home />} />
<Route path="/Profile" element={<ProfilePage />} />
</Route>
<Route path="/" element={<LogInPage />} />
</Routes>
</div>
</div>
);
}

React How to wait till current user data is fetched after user has logged in

In my react app I have guest routes and protected routes. The protected routes are only accessible when the user is authenticated. Next to that, the main app routes are only accessible when the user has signed a contract and finished the onboarding. I'm keeping track of these steps with some extra properties assigned to the user.
My current flow is the following
The user enters the app and the function fetchCurrentUser is triggered inside the AuthContext Provider. If the call to the database returns data the property isAuthenticated is set to true and the user data is set to the currentUser state. If the calls returns an (unauthorized) error isAuthenticated is set to false. Initially isAuthenticated is set to null so I can render a loader as long as isAuthenticated is null.
Let's assume the user wasn't logged in. Since isAuthenticated was first null and now false the code isn't returning the <h1>Loading</h1> loader anymore but will return a route. Because / can't be accessed because isAuthenticated is false, the app will redirect the user to the /login page
When the user fills in the credentials and submits the data a cookie is returned from the backend and set in the browser. Now I want to re-trigger the fetchCurrentUser function to collect the user information. * To do this I set isAuthenticated back to null and I navigate the user to the dashboard page /. Since isAuthenticated is null the spinner will show up instead but the route is already /.
In the meantime fetchCurrentUser will do the api call with the cookie which will return the user data and set isAuthenticated to true.
Short note for step 3 and 4. I think there are better ways to handle this so please don't hesitate to share a better solution.
Maybe there is a way to call the fetchCurrentUser from the Login component and wait till the data is set and navigate the user afterwards? Because fetchCurrentUser is more than an api call and the submit function has to wait till the whole function is done I should work with a promise but inside a promise I can't use async/wait to wait for the api call.
Now that isAuthenticated is true and the user data is known and stored in the AuthProvider the routes can be rendered again. Since / is a protected route the code will check if isAuthenticated is true and check to which route the user needs to be navigated. This part goes wrong Warning: Maximum update depth exceeded but I don't know what I'm missing.
Code to test things out with some fake calls the represent the flow via https://codesandbox.io/s/zealous-meitner-9t000f?file=/src/router/index.js
Login.js
const Login = () => {
const { setIsAuthenticated } = useContext(AuthContext);
const navigate = useNavigate();
const resolver = useYupResolver(loginValidationSchema);
const {
handleSubmit,
register,
formState: { errors },
} = useForm({ resolver });
const submit = async (values) => {
await authService.login(values);
setIsAuthenticated(null);
navigate('/');
};
return (
<div className='w-full border border-grey-300 rounded-lg overflow-hidden shadow sm:mx-auto sm:w-full sm:max-w-md'>
<div className='p-4'>
<form onSubmit={handleSubmit(submit)} className='space-y-6'>
<Input type='text' label='email' name='email' register={register} error={errors.email} />
<Input type='password' label='password' name='password' register={register} error={errors.password} />
<Button type='submit' label='login' color='tenant-primary' />
</form>
</div>
</div>
);
};
AuthContext
const AuthProvider = ({ children }) => {
const [isAuthenticated, setIsAuthenticated] = useState(null);
const [currentUser, setCurrentUser] = useState(null);
useEffect(() => {
if (isAuthenticated !== false) {
getCurrentUser();
}
}, [isAuthenticated]);
const getCurrentUser = async () => {
try {
const { data } = await authService.me();
setIsAuthenticated(true);
setCurrentUser(data);
} catch (error) {
setIsAuthenticated(false);
setCurrentUser(null);
}
};
return <AuthContext.Provider value={{ isAuthenticated, setIsAuthenticated, getCurrentUser, currentUser }}>{children}</AuthContext.Provider>;
};
router.js
const Router = () => {
const authContext = useContext(AuthContext);
const GuestRoute = () => {
return !authContext.isAuthenticated ? <Outlet /> : <Navigate to='/' replace />;
};
const ProtectedRoutes = () => {
if (!authContext.isAuthenticated) return <Navigate to='/login' replace />;
else if (!authContext.currentUser?.settings?.is_contract_signed) return <Navigate to='/contract/sign' replace />;
else if (!authContext.currentUser?.settings?.is_onboarding_finished) return <Navigate to='/onboarding' replace />;
else return <Outlet />;
};
if (authContext.isAuthenticated === null) {
return <h1>Loading ...</h1>;
}
return (
<Routes>
<Route element={<GuestRoute />}>
<Route path='/login' element={<Login />} />
</Route>
<Route element={<ProtectedRoutes />}>
<Route path='/' element={<Navigate to='/onboarding' replace />} />
<Route path='/contract/sign' element={<SignContract />} />
<Route path='/onboarding' element={<Onboarding />} />
<Route path='/profile' element={<Profile />} />
</Route>
<Route path='404' element={<NotFound />} />
<Route path='*' element={<Navigate to='/404' replace />} />
</Routes>
);
};
Issue
The is caused by the ProtectedRoutes unconditionally redirecting to authenticated routes.
const ProtectedRoutes = () => {
if (!authContext.isAuthenticated)
return <Navigate to="/login" replace />; // <-- here
else if (!authContext.currentUser?.settings?.is_contract_signed)
return <Navigate to="/contract/sign" replace />; // <-- here
else if (!authContext.currentUser?.settings?.is_onboarding_finished)
return <Navigate to="/onboarding" replace />; // <-- here
else return <Outlet />;
};
This rerenders the route which rerenders the ProtectedRoutes component which triggers another redirect, repeat ad nauseam.
Solution
The ProtectedRoutes component should only concern itself with protecting access to a route or redirecting to another route to authenticate. The redirecting to specific protected routes based on user properties should occur in the login logic.
Additionally I highly recommend moving the GuestRoute and ProtectedRoutes component declarations out of the Router component. When these components are redeclared each render cycle it will necessarily unmount and remount their entire sub-ReactTree.
router/index.js
import React, { useContext } from "react";
import { Routes, Route, Outlet, Navigate } from "react-router-dom";
import Login from "../pages/Login";
import SignContract from "../pages/SignContract";
import Onboarding from "../pages/Onboarding";
import Dashboard from "../pages/Dashboard";
import { AuthContext } from "../context/AuthContext";
const GuestRoute = () => {
const authContext = useContext(AuthContext);
return !authContext.isAuthenticated ? (
<Outlet />
) : (
<Navigate to="/" replace />
);
};
const ProtectedRoutes = () => {
const authContext = useContext(AuthContext);
return authContext.isAuthenticated ? (
<Outlet />
) : (
<Navigate to="/login" replace />
);
};
const Router = () => {
const authContext = useContext(AuthContext);
if (authContext.isAuthenticated === null) {
return <h1>Loading...</h1>;
}
return (
<Routes>
<Route element={<GuestRoute />}>
<Route path="/login" element={<Login />} />
</Route>
<Route element={<ProtectedRoutes />}>
<Route path="/" element={<Navigate to="/dashboard" replace />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/contract/sign" element={<SignContract />} />
<Route path="/onboarding" element={<Onboarding />} />
</Route>
</Routes>
);
};
export default Router;
pages/Login.js
The handleSubmit handler should receive the returned user object from the auth service and check the "roles" here to redirect to the appropriate authenticated route.
const handleSubmit = async (e) => {
e.preventDefault();
const data = await authService.login(formData);
if (data) {
const user = await authService.me();
if (user) {
authContext.setIsAuthenticated(true);
if (user?.settings?.is_contract_signed) {
navigate("/contract/sign", {replace: true});
} else if (user?.settings?.is_onboarding_finished) {
navigate("/onboarding", {replace: true} );
} else {
navigate("/", { replace: true});
}
}
}
};
The login logic might also want to implement this in a try/catch to handle any Promise rejections and other thrown errors, and also handle the case where the authentication fails. For example, should there be an error message, or redirect to another special page, etc. Basically this code should handle both the happy and unhappy paths.
It shows "Warning: Maximum update depth exceeded" because when you call getCurrentUser() method if isAuthenticated not false the method triggered 2 states and React sees that there are two states and React updated two states in the same time (automatic batching) and when isAuthenticated get new value useEffect() works again it sees that isAuthenticated true ant it is again changes states it happens again again and to infinity for that it show that error

How to skip login page if user is already authenticated in React

My react app has the following routes and contexts:
const App = () =>
<AuthContextProvider>
<IntelContextProvider>
<Routes>
<Route path="/" element={<Layout />}>
<Route path="login" element={<Authentication />} />
<Route path="register" element={<Registration />} />
<Route element={<RequireAuth />}>
<Route path="/" element={<Home />} />
</Route>
<Route path="*" element={<NotFound />} />
</Route>
</Routes>
</IntelContextProvider>
</AuthContextProvider>
Authentication uses an access token stored in memory (AuthContextProvider) and a refresh token stored in a HttpOnly cookie.
My home path uses a protected route implemented as follows:
export const RequireAuth = () => {
const location = useLocation()
const {auth} = useAuth()
return (auth?.username ?
<Outlet/> :
<Navigate to='/login' state={{from: location}} replace/> )
}
If the user is not authenticated, it is redirected to /login without any issue. However, I need to do the opposite too:
If the user has a valid refresh token, when the page is refreshed or the user requests '/register' or '/login pages', then I want the route to be redirected to Home component again.
I tried to put do the following in the Authentication component:
const Authentication = () => {
const [authenticated, setAuthenticated] = useState(null)
const {values, errors, handleChange, handleSubmit} = useAuthenticationForm(validate)
const silentlySignIn = useSilentSignIn()
useEffect(() => {
const silentlyLogin = async () => {
const isAuthenticated = await silentlySignIn()
if(isAuthenticated) setAuthenticated(true)
}
silentlyLogin()
// eslint-disable-next-line
}, [])
return (
authenticated ? <Navigate to='/'/> :
<main className="authentication">
<form>
...
</form>
</main>
);
}
Here is my AuthContext:
export const AuthContextProvider = ({ children }) => {
const [auth, setAuth] = useState()
return (
<AuthContext.Provider value={{ auth, setAuth }}>
{children}
</AuthContext.Provider>
)
}
And Here is my useSignIn hook:
const useSignIn = () => {
const [success, setSuccess] = useState(false)
const [error, setError] = useState()
const { setAuth } = useAuth()
const signIn = async (payload) => {
try {
setError(null)
const { headers: { authorization: token }, data: { uuid } } = await axiosPrivate.post(`/login`, payload)
setAuth({ token, uuid, username: payload.username })
setSuccess(true)
} catch (error) {
console.log(error.message)
if (!error.response) setError('Sistema temporariamente indisponível.')
else if (error.response.status === 401) setError('Usuário ou senha inválidos.')
else setError('Algo deu errado.')
}
}
return [signIn, success, error]
}
Here is my useSilentSignIn (to get a new access token if the refresh token is still valid):
const useSilentSignIn = () => {
const { auth, setAuth } = useAuth()
const silentlySignIn = async () => {
try {
if (auth?.uuid) return false
const response = await axiosPrivate.get('/refresh-token')
const token = response.headers.authorization
const uuid = response.data.uuid
const username = response.data.username
setAuth(prev => ({ ...prev, token, uuid, username }))
return true
} catch (error) {
console.log('Logged out. Please sign in.')
return false
}
}
return silentlySignIn
}
I "solved" the problem, but it first renders the login, then navigates to '/' (due to React component lifecycle). It does not seem like a good solution, it is ugly, and I would need to do the same for '/register' or any similar route.
How to implement something efficient for such a problem?
Github of the project: https://github.com/lucas-ifsp/CTruco-front
Thanks
Your authenticated state has three possible states (yay JavaScript):
Authenticated (true)
Non-authenticated (false)
Not yet known (null)
You could convert them to string enums for clarity, but for conciseness, this is how you would handle all three cases:
if (authenticated === null) return <Spinner /> // Or some other loading indicator
return (
authenticated ? <Navigate to='/'/> :
<main className="authentication">
<form>
...
</form>
</main>
);
There's no need to add this logic to the Authentication rendered on the "/login" path. In this case you create another route protection component that does the inverse of the RequireAuth component. This is commonly referred to as an "anonymous route" that you only want users that are not authenticated to access.
If the user is authenticated then render a redirect to any non-anonymous path, otherwise render the outlet for the nested route to render its element into. While the auth status is being checked and still undefined, you can render null or any sort of loading indicator to make the route protection wait until the state value updates.
Example:
export const AnonymousRoute = () => {
const { auth } = useAuth();
if (auth === undefined) {
return null; // or loading indicator/spinner/etc...
}
return auth.username
? <Navigate to='/' replace />
: <Outlet/>;
}
...
<Routes>
<Route element={<Layout />}>
<Route element={<AnonymousRoute />}>
<Route path="login" element={<Authentication />} />
<Route path="register" element={<Registration />} />
</Route>
<Route element={<RequireAuth />}>
<Route path="/" element={<Home />} />
</Route>
<Route path="*" element={<NotFound />} />
</Route>
</Routes>

How to fix public route in react router v6 showing the login for a spli second

I have a problem with public and private route. In which when the user is already authenticated the private and public routes work. But for some reason when I refresh to the homepage it shows the login page for a split second.
How do i fix this?
PRIVATE ROUTE
const PrivateRoute = () => {
const auth = useAuth();
if (!auth.user) {
return <Navigate to="/login" />;
}
return <Outlet />;
};
PUBLIC ROUTE
const PublicRoute = () => {
const auth = useAuth();
if (auth.user) {
return <Navigate to="/" />;
}
return <Outlet />;
};
APP
<Routes>
<Route element={<PublicRoute />}>
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
</Route>
<Route element={<PrivateRoute />}>
<Route path="/" element={<Home />} />
<Route path="/message" element={<Message />} />
</Route>
</Routes>
You should ensure that your "unauthenticated" state value doesn't match the "I don't know the authentication status" state value. What this means for example, if auth.user being true or some defined user object equals a user being considered "authenticated" and if a auth.user being false or null equals a user being considered "unauthenticated" then you shouldn't use an initial auth.user that is either true|<user object> or false|null.
In other words you want to use some indeterminant value to indicate the authentication status has yet to be determined, and in this indeterminant state you can render null or some loading indicator until the authentication status resolves.
You tagged with firebase so I'll be assuming that somewhere in your code is an onAuthStateChanged call that returns either the user object or null when there is no authenticated user. In this case the user comparison will be against undefined. Just ensure that the default auth.user value is undefined.
Assuming something similar:
export default function useAuth() {
const [auth, setAuth] = useState({}); // user undefined
const user = useFirebase();
useEffect(() => {
const unsubscribe = onAuthStateChanged(
user.auth,
(user) => setAuth({
user: user ?? false
})
);
return unsubscribe;
}, [user]);
return auth;
}
Example:
const PrivateRoute = () => {
const auth = useAuth();
if (auth?.user === undefined) {
return null; // or loading indicator/spinner/etc
}
return auth.user
? <Outlet />
: <Navigate to="/login" replace />;
};
const PublicRoute = () => {
const auth = useAuth();
if (auth?.user === undefined) {
return null; // or loading indicator/spinner/etc
}
return auth.user
? <Navigate to="/" replace />
: <Outlet />;
};

How to wait state before rendering in React JS?

I use 'React Context' to pass state user to the child components.
The problem: Everytime you reload the page, the state user value is null. This cause the page briefly redirect to /login before redirecting to '/dashboard`. This will prevent user from accessing a page manually.
The goal: How to wait for state user before rendering?
App.js
function App() {
const [user, setUser] = useState(null);
useEffect(() => {
firebase.auth().onAuthStateChanged((user) => {
if (user) setUser(user);
else setUser(null);
});
}, []);
return (
<Router>
<AuthDataContext.Provider value={user}>
<Layout>
<Routes />
</Layout>
</AuthDataContext.Provider>
</Router>
);
}
routes
export const Routes = () => {
const user = useContext(AuthDataContext);
if (user) {
return (
<Switch>
<Route path="/dashboard" component={Dashboard} />
<Route path="/list" component={List} />
<Redirect to="/dashboard" />
</Switch>
);
} else {
return (
<Switch>
<Route path="/login" component={LogIn} />
<Route path="/register" component={Register} />
<Route path="/passwordreset" component={PasswordReset} />
<Redirect to="/login" />
</Switch>
);
}
};
authdata
import React from "react";
export const AuthDataContext = React.createContext(null);
edit: quick fix
App.js
const [user, setUser] = useState("first");
routes
else if (user === "first") return null
You can have a component that initiates API call and renders some kind of loader while user is waiting for a response. Then in case user is authenticated you can render regular application routes, if not – render routes for sign in, sign up and so on.

Resources