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>
);
Related
I am using react-router-dom and I am trying to push to the browser history using the history object from the useHistory hook. The path that I push to should trigger some logic within the router which will force a redirect to another path (which renders some content).
My issue is that the <Redirect /> does not seem to be doing anything and I'm not 100% sure I know why. I created a codepen to demonstrate the issue that I am having. (Same as code below). You can see the issue if you manually navigate the browser in the codepen to the main route e.g https://c629mk.csb.app/, you will see no content load.
import React, { useEffect, useState } from "react";
import ReactDOM from "react-dom";
import {
Route,
Switch,
Redirect,
useHistory,
BrowserRouter,
useRouteMatch
} from "react-router-dom";
const HomeRouter = () => {
const { path } = useRouteMatch();
const someRouterSpecificLogic = true;
const initialPath = someRouterSpecificLogic ? "location" : "videos";
return (
<Switch>
<Route path={`${path}/location`} render={() => <h1>Location</h1>} />
<Route path={`${path}/videos`} render={() => <h1>Videos</h1>} />
<Redirect from={`${path}/`} to={`${path}/${initialPath}`} />
</Switch>
);
};
const AboutRouter = () => {
const { path } = useRouteMatch();
return (
<Switch>
<Route path={`${path}/history`} render={() => <h1>History</h1>} />
<Route path={`${path}/background`} render={() => <h1>Background</h1>} />
<Redirect from={`${path}/`} to={`${path}/history`} />
</Switch>
);
};
const useSomeAsyncHook = () => {
const [asyncResult, setAsyncResult] = useState(false);
useEffect(() => {
setTimeout(() => {
setAsyncResult("someValue");
}, 300);
});
return asyncResult;
};
const AppRouter = () => {
const history = useHistory();
const asycnResult = useSomeAsyncHook();
useEffect(() => {
if (asycnResult === "someValue") {
history.push("/home");
}
}, [history, asycnResult]);
return (
<>
<p>There should be other content rendering on this page:</p>
<Switch>
<Route path="/home" component={HomeRouter} />
<Route path="/about" component={AboutRouter} />
<Redirect from="/" to="/home" />
</Switch>
</>
);
};
const App = () => {
return (
<BrowserRouter>
<AppRouter />
</BrowserRouter>
);
};
ReactDOM.render(<App />, document.getElementById("root"));
What I think is happening is this:
/ renders triggering the Redirect to /home
useEffect pushes / to the history again
Redirect logic does not work the second time around
I am wondering if anyone knows how to force the redirect to happen at 3 again?
This can be resolved by adding a loadState to the code. This way the routes / redirect logic only renders after the history.push has taken place.
I was confused because I thought that history.push would update the internal state of browserRouter and trigger a rerender of all child routes/ redirects. This is not the case. In my case history.push did not cause the Redirect component to rerender to it did not trigger another redirect and the user would just see a white screen. Adding a loadstate as shown below resolved the problem for me:
import React, { useEffect, useState } from "react";
import ReactDOM from "react-dom";
import {
Route,
Switch,
Redirect,
useHistory,
BrowserRouter,
useRouteMatch
} from "react-router-dom";
const HomeRouter = () => {
const { path } = useRouteMatch();
const someRouterSpecificLogic = true;
const initialPath = someRouterSpecificLogic ? "location" : "videos";
return (
<Switch>
<Route path={`${path}/location`} render={() => <h1>Location</h1>} />
<Route path={`${path}/videos`} render={() => <h1>Videos</h1>} />
<Redirect from={`${path}/`} to={`${path}/${initialPath}`} />
</Switch>
);
};
const AboutRouter = () => {
const { path } = useRouteMatch();
return (
<Switch>
<Route path={`${path}/history`} render={() => <h1>History</h1>} />
<Route path={`${path}/background`} render={() => <h1>Background</h1>} />
<Redirect from={`${path}/`} to={`${path}/history`} />
</Switch>
);
};
const useSomeAsyncHook = () => {
const [asyncResult, setAsyncResult] = useState(false);
const [asyncResultFetched, setAsyncResultFetched] = useState(false);
useEffect(() => {
setTimeout(() => {
setAsyncResult("someValue");
setAsyncResultFetched(true);
}, 300);
});
return { asyncResult, asyncResultFetched };
};
const AppRouter = () => {
const history = useHistory();
const { asycnResult, asyncResultFetched } = useSomeAsyncHook();
useEffect(() => {
if (asycnResult === "someValue") {
history.push("/home");
}
}, [history, asycnResult]);
if (!asyncResultFetched) {
return <h1>Loading...</h1>;
}
return (
<>
<p>There should be other content rendering on this page:</p>
<Switch>
<Route path="/home" component={HomeRouter} />
<Route path="/about" component={AboutRouter} />
<Redirect from="/" to="/home" />
</Switch>
</>
);
};
const App = () => {
return (
<BrowserRouter>
<AppRouter />
</BrowserRouter>
);
};
ReactDOM.render(<App />, document.getElementById("root"));
Thank you to #Abdulrahman Ali and #Drew Reese for their assistance in helping me realise what my issue was in the comments of my question above
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>
);
}
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 am new to React Hooks and tried to play around with them + the experimental React Router version 6 library, so I don't have to upgrade my code from version 5 to 6 at some point in the future
Note: version 6 is not comptaible with version 5, so no Switch is not the answer ;)
Idea: Create a simple admin application where you can login/logout via button click. The admin page is protected via access lock, so no one can access it via direct URL.
Problem: After clicking the login button, the URL is changed to /admin, but I don't see the admin page (Only after a page refresh or an enforced page refresh via windows.location.reload()).
Is this a problem in React Router v6 or am I using the API the wrong way? Source code (https://codesandbox.io/s/quirky-black-rsdy0):
import React, {useEffect, useState} from "react";
import {BrowserRouter, Route, Routes, useNavigate} from "react-router-dom";
async function isLoggedIn() {
// In real life this function is calling a REST endpoint and awaiting the result
return localStorage.getItem("loggedin") !== null;
}
async function setLoginStatus(status) {
// In real life this function is calling a REST endpoint and awaiting the result
if (status === true) {
localStorage.setItem("loggedin", true);
} else {
localStorage.removeItem("loggedin");
}
}
function LoginPage() {
const navigate = useNavigate();
async function handleLogin(event) {
event.preventDefault();
await setLoginStatus(true);
navigate("/admin"); // --> The user is logged in and the URL changes to /admin, but he doesn't see the admin page (Only after a page refresh)
//window.location.reload(); // Hack to reload the page to see the admin page
}
return (
<form onSubmit={handleLogin}>
<button type="submit">Login</button>
</form>
);
}
function LogoutPage() {
const navigate = useNavigate();
useEffect(() => {
const logoutAccount = async () => {
await setLoginStatus(false);
navigate("/login");
};
logoutAccount();
});
return <p>Logging out</p>;
}
function AdminPage() {
return (
<div>
<p>The secured admin page</p>
Logout
</div>
);
}
function AuthenticationLock(props) {
if (props.isLoggedIn === true) {
return <div>{props.children}</div>;
} else {
return <LoginPage/>;
}
}
export default function App() {
const [loginStatus, setAppLoginStatus] = useState(false);
useEffect(() => {
const login = async () => {
const value = await isLoggedIn();
setAppLoginStatus(value);
};
login();
});
return (
<BrowserRouter>
<AuthenticationLock isLoggedIn={loginStatus}>
<Routes>
<Route path="/" element={<AdminPage/>}/>
<Route path="/admin" element={<AdminPage/>}/>
<Route path="/login" element={<LoginPage/>}/>
<Route path="/logout" element={<LogoutPage/>}/>
<Route path="/*" element={<AdminPage/>}/>
</Routes>
</AuthenticationLock>
</BrowserRouter>
);
}
Somehow my AuthenticationLock was repsonsible for the problem. I changed to a different design and now it works:
import React, { useEffect } from "react";
import {
BrowserRouter,
Route,
Routes,
Link,
useNavigate
} from "react-router-dom";
function isLoggedIn() {
// In real life this function is calling a REST endpoint and awaiting the result
return localStorage.getItem("loggedin") !== null;
}
async function setLoginStatus(status) {
// In real life this function is calling a REST endpoint and awaiting the result
if (status) {
localStorage.setItem("loggedin", "1");
} else {
localStorage.removeItem("loggedin");
}
}
function DashboardPage() {
return (
<div>
<h1>Dashboard page</h1>
<p>This is the dashboard page</p>
<Link to={"/accounts"}>Accounts</Link>
<br />
<Link to={"/logout"}>Logout</Link>
</div>
);
}
function AccountsPage() {
return (
<div>
<h1>Accounts page</h1>
<p>This is the accounts page</p>
<Link to={"/dashboard"}>Dashboard</Link>
<br />
<Link to={"/logout"}>Logout</Link>
</div>
);
}
function LoginPage() {
const navigate = useNavigate();
async function handleLogin(event) {
event.preventDefault();
await setLoginStatus(true);
navigate("/dashboard");
}
return (
<div>
<h1>Login</h1>
<form onSubmit={handleLogin}>
<button type="submit">Login</button>
</form>
</div>
);
}
function LogoutPage() {
const navigate = useNavigate();
useEffect(() => {
const logoutAccount = async () => {
await setLoginStatus(false);
navigate("/login");
};
logoutAccount();
});
return <p>Logging out</p>;
}
const PrivateRoute = ({ component, ...rest }) => {
if (isLoggedIn()) {
const routeComponent = props => React.createElement(component, props);
return <Route {...rest} render={routeComponent} />;
} else {
return <LoginPage />;
}
};
function App() {
return (
<BrowserRouter>
<Routes>
<PrivateRoute path="/" element={<DashboardPage />} />
<PrivateRoute path="/dashboard" element={<DashboardPage />} />
<PrivateRoute path="/accounts" element={<AccountsPage />} />
<Route path="/login" element={<LoginPage />} />
<PrivateRoute path="/logout" element={<LogoutPage />} />
<PrivateRoute path="/*" element={<DashboardPage />} />
</Routes>
</BrowserRouter>
);
}
export default App;
Wrapp all your routes around <Switch></Switch> and set the prop exact={true} on all routes except that last one.
import {BrowserRouter, Route, Routes, Switch, useNavigate} from "react-router-dom";
[...]
return (
<BrowserRouter>
<AuthenticationLock isLoggedIn={loginStatus}>
<Routes>
<Switch>
<Route path="/" exact={true} element={<AdminPage/>}/>
<Route path="/admin" exact={true} element={<AdminPage/>}/>
<Route path="/login" exact={true} element={<LoginPage/>}/>
<Route path="/logout" exact={true} element={<LogoutPage/>}/>
<Route path="/*" element={<AdminPage/>}/>
</Switch>
</Routes>
</AuthenticationLock>
</BrowserRouter>
);
I have a react + firebase application which has protected routes. I face issue if a logged in user accesses the login page. The issue is that the login page gets displayed for a second and then redirects to the home page i.e protected route. I feel the issue is because the value retrieved from context in the login page to check if user is authenticated gets updated after the route is resolved. Can someone give me pointers on how should I fix this. Ideally I would not want the user to see the login page for sometime if the user is already authenticated.
//App.js
render() {
return (
<AuthProvider>
<Router>
<Switch>
<PrivateRoute exact path="/" component={Home}></PrivateRoute>
<Route exact path="/login" component={LoginPage}></Route>
<Route exact path="/signup" component={SignUp}></Route>
</Switch>
</Router>
</AuthProvider>
);
}
}
//AuthProvider
import React, { useEffect, useState } from "react"
import { fire } from "./Fire"
export const AuthContext = React.createContext();
//this component will maintain the current user throughout the app
export const AuthProvider = ({ children }) => {
const [currentUser, setCurrentUser] = useState(null)
//empty array as second arg to useEffect hook as we only want to trigger it once
useEffect(() => {
console.log("useEffect")
fire.auth().onAuthStateChanged(setCurrentUser)
}, [])
return (
<AuthContext.Provider value={{ currentUser }}>{children}</AuthContext.Provider>
)
}
//PrivateRoute
const PrivateRoute = ({ component: RouteComponent, ...rest }) => {
//useContext hook makes it very easy to retrieve the value
const { currentUser } = useContext(AuthContext)
return (
<Route {...rest} render={
routeProps => {
console.log("currentUser" + currentUser);
return !!currentUser ? (
<RouteComponent {...routeProps} />
) : (
<Redirect to={"/login"} />
)
}
} />
)
}
//login
render() {
if (this.props.context.currentUser)
return <Redirect to="/" />
return (
<Login email={this.state.email} password={this.state.password} inputHandler={this.onInputChange} loginHandler={this.onLoginClick} />
)
}
You should probably add a loading check in your PrivateRoute file which will just show a Loading... or a loader if the user is not loaded yet.
For that you will have to do some minor changes in your AuthProvider file and in PrivateRoute.
//PrivateRoute
const PrivateRoute = ({ component: RouteComponent, ...rest }) => {
//useContext hook makes it very easy to retrieve the value
const { currentUser } = useContext(AuthContext)
return (
<Route {...rest} render={
routeProps => {
console.log("currentUser" + currentUser);
return !!currentUser ? (
<RouteComponent {...routeProps} />
) : currentUser === 'loading' ? <h1>Loading...</h1>
:(
<Redirect to={"/login"} />
)
}
} />
)
}