React / firebase app flashes login screen before auth finishes - javascript

When you signup/ login/ refresh the page the login page flashes just before going to the correct page. I tried putting a loader in but all it does is do a loader animation before it flashes then goes to correct page. Any idea how to get this to not happen?
function App(){
const [user, setUser] = useState();
const [loading, setLoad] = useState(true);
useEffect(() => {
const unsubscribe = onAuthStateChanged(getAuth(), setUser, setLoad(false))
return () => {unsubscribe()}
}, [])
const AuthenticatedRoute = ({children}) => {
let isAuthenticated;
if(user !=null){
isAuthenticated = true;
}else{
isAuthenticated = false;
}
if(!loading){
return isAuthenticated? children: <Navigate to="/signin"/>
}else{
return <Loading/>
}
}
const UnauthenticatedRoute = ({children}) => {
let isAuthenticated;
if(user !=null){
isAuthenticated = true;
}else{
isAuthenticated = false;
}
if(!loading){
return !isAuthenticated? children: <Navigate to="/home"/>
}else{
return <Loading/>
}
}
return(
<Router>
<div className="App">
{
<Routes>
<Route exact path="/" element={<UnauthenticatedRoute><PreHome/></UnauthenticatedRoute>}/>
<Route path='/home' element={<AuthenticatedRoute><Home/></AuthenticatedRoute>} />
<Route exact path="/signin" element={<UnauthenticatedRoute><Signin/></UnauthenticatedRoute>} />
<Route exact path="/signup" element={<UnauthenticatedRoute><Signup/></UnauthenticatedRoute>} />
</Routes>
}
</div>
</Router>
)
}
Sign Out code:
This one line has been working for signing out
<button onClick={() => signOut(getAuth())}>Sign Out</button>
Sign In code:
async function OnFormSubmit(e){
e.preventDefault();
const auth = getAuth();
try{
isLoading(true);
await signInWithEmailAndPassword(auth, email, pw)
isLoading(false);
}catch(err){
console.log(err)
}
}

Issues
The loading state is only cleared when there's an error in onAuthStateChanged.
The route protector components are declared inside another React component. This is anti-pattern. When new "instances" of these components are declared each time the parent App component rerenders, it will unmount/mount the component's subReactTree.
The components don't wait for the user's authentication status to resolve before deciding to redirect or render the routed content.
Solution
Move the route protector components out on their own. Render an Outlet for nested routes to be rendered into. This allows you to render these as layout routes instead of individual wrapper components.
Use a proper "loading" state. Use undefined user state as the "loading state".
Example:
import { Outlet, Navigate } from 'react-router-dom';
const AuthenticatedRoute = ({ user }) => {
if (user === undefined) return <Loading />;
return user
? <Outlet />
: <Navigate to="/signin" />;
};
const UnauthenticatedRoute = ({ user }) => {
if (user === undefined) return <Loading />;
return user
? <Navigate to="/home" />
: <Outlet />;
};
...
function App(){
const [user, setUser] = useState();
useEffect(() => {
const unsubscribe = onAuthStateChanged(getAuth(), setUser);
return unsubscribe;
}, []);
return(
<AuthContextProvider>
<Router>
<div className="App">
<Routes>
<Route element={<UnauthenticatedRoute user={user} />}>
<Route path="/" element={<PreHome />} />
<Route path="/signin" element={<Signin />} />
<Route path="/signup" element={<Signup />} />
</Route>
<Route element={<AuthenticatedRoute user={user} />}>
<Route path='/home' element={<Home />} />
</Route>
</Routes>
</div>
</Router>
</AuthContextProvider>
);
}

Related

"Functions are not valid as a React child" during replacement react-router-dom v5 to v6

I tried to solve this problem in several ways without success. This Router work perfect with render but when I replace by element then the App.jsx throws me an error.
If I delete a () => in Route with Homepage than Homepage component will be render, but I can't do this in Route with signin/signup.
I can't figure out where the error could be.
import React, { useState, useEffect } from "react";
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
import Homepage from "./Pages/Homepage/Homepage";
import SignIn from "./Pages/Authentication/SignIn";
import SignUp from "./Pages/Authentication/SignUp";
import Dashboard from "./Pages/Dashboard/Dashboard";
import "react-toastify/dist/ReactToastify.css";
import { ToastContainer } from "react-toastify";
const App = () => {
const [isLoading, setIsLoading] = useState(true);
const [isAuthenticated, setIsAuthenticated] = useState(false);
const checkAuthenticated = async () => {
try {
const res = await fetch("/api/auth/verify", {
method: "POST",
headers: { jwtToken: localStorage.token },
});
const parseRes = await res.json();
parseRes === true ? setIsAuthenticated(true) : setIsAuthenticated(false);
setIsLoading(false);
} catch (err) {
console.error(err.message);
}
};
useEffect(() => {
checkAuthenticated();
}, []);
const setAuth = (boolean) => {
setIsAuthenticated(boolean);
};
return (
<>
{isLoading ? null : (
<BrowserRouter>
<Routes>
<Route
exact
path="/signin"
element={(props) =>
!isAuthenticated ? (
<SignIn {...props} setAuth={setAuth} />
) : (
<Navigate to="/dashboard/overview" />
)
}
/>
<Route
exact
path="/signup"
element={((props) =>
!isAuthenticated ? (
<SignUp {...props} setAuth={setAuth} />
) : (
<Navigate to="/dashboard/overview" />
)
)}
/>
<Route
path="/dashboard"
element={(props) =>
isAuthenticated ? (
<Dashboard {...props} setAuth={setAuth} />
) : (
<Navigate to="/signin" />
)
}
/>
<Route exact path="/" element={() => <Homepage />} />
</Routes>
</BrowserRouter>
)}
</>
);
};
export default App;
The Route component changed significantly from react-router-dom#5 to react-router-dom#6. There are no longer component or render or children function props, instead replaced by a single element prop taking a ReactNode, a.k.a. JSX.
const App = () => {
...
return (
<>
{isLoading ? null : (
<BrowserRouter>
<Routes>
<Route
path="/signin"
element={!isAuthenticated
? <SignIn setAuth={setAuth} />
: <Navigate to="/dashboard/overview" />
}
/>
<Route
path="/signup"
element={!isAuthenticated
? <SignUp setAuth={setAuth} />
: <Navigate to="/dashboard/overview" />
)}
/>
<Route
path="/dashboard"
element={isAuthenticated
? <Dashboard setAuth={setAuth} />
: <Navigate to="/signin" />
}
/>
<Route path="/" element={<Homepage />} />
</Routes>
</BrowserRouter>
)}
</>
);
};
export default App;
Route protection is so common that there is layout route pattern for it. Create layout routes that consume the isAuthenticated state and render null or some loading indicator while the authentication status is fetched, then conditionally renders an Outlet for the nested routes to render their content into or a redirect.
Example:
import { Navigate, Outlet } from 'react-router-dom';
const ProtectedRoutes = ({ isAuthenticated }) => {
if (isAuthenticated === undefined) {
return null; // or loading indicator, etc..
}
return isAuthenticated
? <Outlet />
: <Navigate to="/signin" replace />;
}
const AnonymousRoutes = ({ isAuthenticated }) => {
if (isAuthenticated === undefined) {
return null; // or loading indicator, etc..
}
return isAuthenticated
? <Navigate to="/dashboard/overview" replace />
: <Outlet />;
}
...
const App = () => {
const [isAuthenticated, setIsAuthenticated] = useState(); // initially undefined
useEffect(() => {
const checkAuthenticated = async () => {
try {
const res = await fetch("/api/auth/verify", {
method: "POST",
headers: { jwtToken: localStorage.token },
});
const parseRes = await res.json();
setIsAuthenticated(!!parseRes);
} catch (err) {
console.error(err.message);
}
};
checkAuthenticated();
}, []);
const setAuth = (boolean) => {
setIsAuthenticated(boolean);
};
return (
<BrowserRouter>
<Routes>
<Route element={<AnonymousRoutes isAuthenticated={isAuthenticated} />}>
<Route path="/signin" element={<SignIn setAuth={setAuth} />} />
<Route path="/signup" element={<SignUp setAuth={setAuth} />} />
</Route>
<Route element={<ProtectedRoutes isAuthenticated={isAuthenticated} />}>
<Route path="/dashboard" element={<Dashboard setAuth={setAuth} />} />
</Route>
<Route path="/" element={<Homepage />} />
</Routes>
</BrowserRouter>
);
};
export default App;
Note also that because there are no render functions for the routed components that there are no longer any route props (i.e. history, location, match). These were replaced by React hooks, i.e. useNavigate, useLocation, and useParams. Use the hooks the components if they need to access these.

Redirect to dashboard after successful payment

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

React-Router-Dom changing states of the page

Hi guys i have a problem. When I enter "dashboard" which is a private route it redirects me to "login" first then to dashboard. True and False are playing together. How can i fix it to not redirect me to login then to dashboard.
video example:
https://cdn.aboutluc.xyz/images/rc64kb6af92sswn3r4mv.mp4
code:
import React, {
useState,
useEffect
} from "react"
import { toast } from "react-toastify"
import {
BrowserRouter as Router,
Routes,
Route,
Navigate
} from "react-router-dom"
import {
Login,
Register,
Dashboard,
} from "./Pages"
import {
Navbar
} from "./Components"
import './App.css'
import "react-toastify/dist/ReactToastify.css"
import 'bootstrap/dist/css/bootstrap.min.css'
toast.configure()
const App = () => {
const [ isAuthenticated, setIsAuthenticated ] = useState()
const setAuth = (boolean) => {
setIsAuthenticated(boolean)
}
const isAuth = async () => {
try {
const res = await fetch("http://localhost:5000/api/auth/verify", {
headers: { JwtToken: localStorage.JwtToken }
});
const parseRes = await res.json();
parseRes === true ? setIsAuthenticated(true) : setIsAuthenticated(false);
} catch (error) {
console.error(error)
}
}
useEffect(() => {
isAuth()
}, [])
return (
<>
<Router>
<Navbar setAuth={setAuth} isAuthenticated={isAuthenticated} />
<Routes>
<Route
exact
path="/login"
element={
isAuthenticated ? (
<Navigate replace={true} to="/dashboard" />
) : (
<Login setAuth={setAuth} />
)
}
/>
<Route
exact
path="/register"
element={
isAuthenticated ? (
<Navigate replace={true} to="/dashboard" />
) : (
<Register setAuth={setAuth} />
)
}
/>
<Route
exact
path="/dashboard"
element={
isAuthenticated ? (
<Dashboard setAuth={setAuth} />
) : (
<Navigate replace={true} to="/login" />
)
}
/>
</Routes>
</Router>
</>
)
}
export default App
The possible issue I see is the "gap" on the initial render where the isAuthenticated state is undefined and the useEffect hook callback to set that state hasn't run yet. If you attempt to directly access a protected route then regardless of actual auth status the code will bounce you to the login route.
For this you typically want to use the "third" indeterminant state to "hold" on either redirecting to auth or allowing access through to the protected component until the auth status is confirmed.
Abstract the auth status into auth layout components.
const AuthLayout = ({ isAuthenticated }) => {
if (isAuthenticated === undefined) return null; // or loading spinner, etc...
return isAuthenticated
? <Outlet />
: <Navigate to="/login" replace />;
};
const AnonymousLayout = ({ isAuthenticated, to }) => {
if (isAuthenticated === undefined) return null; // or loading spinner, etc...
return isAuthenticated
? <Navigate to={to} replace />
: <Outlet />;
};
User the layouts to guard/protect specific routes.
<Routes>
<Route
element={(
<AnonymousLayout isAuthenticated={isAuthenticated} to="/dashboard" />
)}
>
<Route path="/login" element={<Login setAuth={setAuth} />} />
<Route path="/register" element={<Register setAuth={setAuth} />} />
</Route>
<Route element={<AuthLayout isAuthenticated={isAuthenticated} />}>
<Route path="/dashboard" element={<Dashboard setAuth={setAuth} />} />
</Route>
</Routes>
Note: You only ever call isAuth when the App component mounts. You may want to call this function or otherwise validate your auth token a little more often than this. Passing isAuth into the route wrappers and invoking also in an useEffect hook probably isn't a terrible idea.

how to redirect in react v6

I need to navigate back to the original requested URL after login.
For example, the user enters www.eCart.com/Cart as the user is not authenticated, it will navigate to the login page www.eCart.com/login.
Once authenticated, it should navigate back to www.eCart.com/Cart automatically
my protectedRoute.js looks like this
import React from 'react'
import { connect } from 'react-redux'
import { Navigate, Outlet, useLocation, useNavigate} from 'react-router-dom'
export const withRouter = (Component) => { //works only for react16-17 //hooks
const Wrapper = (props) => {
const location = useLocation()
const navigate = useNavigate();
return (
<Component
navigate = {navigate}
{...props}
location={location}
{...props}
/>
);
};
return Wrapper;
};
const ProtectedRoute = ({component:Component, auth,...rest}) => (
auth.isAuthenticated ? <Outlet /> : <Navigate to={`/login/${rest.location.search}`} replace />
)
const mapStateToProps = (state) => ({
auth: state.auth
})
export default connect(mapStateToProps)(withRouter(ProtectedRoute))
my app.js is like this
function App(props) {
useEffect(() => {
store.dispatch(setCurrentUser())
}, [])
const grabProductsFromStorage = () =>{
const userId = decodeUser().user.id
const cartProducts = JSON.parse(localStorage.getItem("products"))
const context = {products: cartProducts, userId}
store.dispatch(addToCart(context))
localStorage.removeItem("products")
}
if(localStorage.getItem("token") && localStorage.getItem("products")){
grabProductsFromStorage()
}
return (
<Provider store={store}>
<Router>
<Routes>
<Route exact path="/" element={<Landing/>} />
<Route exact path="/products/:id" element={<ProductDetails/>} />
<Route exact path="/" element={<ProtectedRoute/>}>
<Route exact
path="/dashboard/*"
element={<Dashboard {...props} nestedRoute={Home} />}
/>
<Route exact path="/cart" element={<Cart />} />
</Route>
<Route exact path="/register" element={<Register/>} />
<Route exact path="/login" element={<Login/>} />
</Routes>
</Router>
</Provider>
);
}
Also, I've seen somewhere to use state in Navigate with the location it but when I'm doing it it's throwing an error of Unexpected use of 'location'
You need to store that cart route first. while redirecting to login page from the cart if a user is not authenticated & redirected to the login page you need to store the cart route in your localstorage or somewhere in your state so after login you can check do we have afterlogin route then you can redirect the user to that page.
There are some approaches:
Redirect user to another page:
function Redirect() {
let navigate = useNavigate();
function handleClick() {
navigate('/home')
}
return (
<div>
<button onClick={handleClick}>go home</button>
</div>
);
}
redirect to previous page
function Redirect() {
let navigate = useNavigate();
function handleClick() {
navigate(-1)
}
return (
<div>
<button onClick={handleClick}>go home</button>
</div>
);
}
redirect user to the next page
function Redirect() {
let navigate = useNavigate();
function handleClick() {
navigate(1)
}
return (
<div>
<button onClick={handleClick}>go home</button>
</div>
);

Is there a way to halt loading a react app until authentication status is resolved?

I'm loading a react app that has a root component App.js which is determining if an authenticated user exists through a central redux store, but the problem is it takes a fraction of a second to resolve. Until then the user is getting a flash of the login page even if the user is logged in, which I'm sure counts as bad user experience. Is there a way to not show anything at all until the the status is resolved. I'm attaching a code snippet.
function App(props) {
const cookies = new Cookies();
const { user } = props;
if (cookies.get("authToken") === "null" || cookies.get("authToken") === undefined) {
//console.log("no valid token");
} else {
if (user === null) {
props.fetchLoggedInUser(cookies.get("authToken"));
}
}
const isLoggedIn = user ? (
<div className="App s12 l12 m12">
<Navbar user={user}/>
<Switch>
<Route exact path="/" component={() => (<Home user={user} />)}></Route>
<Route exact path="/create_blog" component={() => (<CreateBlog user={user} />)}></Route>
<Route exact path="/edit_profile" component={() => (<EditProfile user={user} />)}></Route>
</Switch>
</div>
) : (
<div className="App">
<Navbar user={null}/>
<Switch>
<Route exact path="/" component={() => (<Home user={null} />)}></Route>
<Route exact path="/log_in" component={LogIn}></Route>
<Route exact path="/sign_up" component={SignUp}></Route>
</Switch>
</div>
);
return (
<BrowserRouter>
{isLoggedIn}
</BrowserRouter>
);
}
const mapStateToProps = (state) => {
return {
authError: state.auth.authError,
token: state.auth.token,
user: state.auth.user
}
}
const mapDispatchToProps = (dispatch) => {
return {
fetchLoggedInUser: (token) => dispatch(fetchLoggedInUser(token))
}
}
export default connect(mapStateToProps, mapDispatchToProps)(App);
Create a state variable isLoading (or with ay name you want). set its initial value to true.
if (isLoading) {
return '';
} else {
return (
<BrowserRouter>
{isLoggedIn}
</BrowserRouter>
);
}
Once you get the value of isLoggedIn you can set back isLoading to false and react will rerender.

Categories