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>
);
}
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 =>
....,
);
};
here is my protectedroute component
am using react-router-dom v6 and accessing the token from localStorage
and either ways user is always returning undefined
import { Outlet, Navigate} from "react-router-dom";
import axios from "axios";
const ProtectedRoute = () => {
const userAuth = () => {
axios.get("http://localhost:5000/isUserAuth", {
headers: {
"x-access-token": localStorage.getItem("token")
}}).then((response) => {
console.log(response.data)
if(response.data.auth) {
console.log(true)
return true;
} else {
console.log(false)
return false;
}
})
}
let auth = userAuth()
console.log("auth",auth)
return (
auth? <Outlet/> : <Navigate to="/"/>
)
}
export default ProtectedRoute
my app.js
function App() {
return (
<BrowserRouter>
<ToastContainer position='top-center'/>
<Routes>
<Route element={<ProtectedRoutes/>}>
<Route exact path='/home'
element={< Home />}/>
<Route exact path='/add'
element={< AddCust />} />
<Route exact path='/update/:id'
element={< AddCust />} />
<Route exact path='/view/:id'
element={< View />} />
<Route exact path='/table'
element={< Table />} />
<Route exact path='/edit-order/:id'
element={< Table />} />
<Route exact path='/orders'
element={< Orders />} />
</Route>
<Route exact path='/' element={< Login />} />
</Routes>
</BrowserRouter>
);
}
export default App;
this is what is consoled logged
enter image description here
which is weired whether a token exists or not auth is always undefined
Nothing is actually returned from the userAuth function, so auth is undefined. While you could return the axios Promise object, this will make userAuth an asynchronous function and not usable as a ternary condition to conditionally render the Outlet component or redirect.
A solution then is to convert auth to a React state, updated in the GET request flow, and conditionally render null or a loading indicator until the auth status resolves.
Example:
const ProtectedRoute = () => {
const { pathname } = useLocation();
const [auth, setAuth] = React.useState(); // initially undefined
React.useEffect(() => {
const checkAuth = async () => {
try {
const response = await axios.get(
"http://localhost:5000/isUserAuth",
{
headers: {
"x-access-token": localStorage.getItem("token")
},
}
);
setAuth(!!response.data.auth);
} catch(error) {
// handle error, log, etc...
setAuth(false);
}
};
checkAuth();
}, [pathname]); // trigger auth check on route change
if (auth === undefined) {
return null; // loading indicator/spinner/etc
}
return auth
? <Outlet/>
: <Navigate to="/" replace state={{ from: pathname }} />;
};
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.
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>
);
}
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.