ReactJS: Redirecting to a new page within an axios request - javascript

I am building a MERN app with a login/register system and I am stuck on being able to redirect a user to a confirmation page which then prompts them to login.
It seems like I could use the useHistory hook in react-router-dom and do history.push() within my axios request which is within my register function:
function handleRegister(e) {
let history = useHistory();
e.preventDefault();
// Grab state
const user = {
username: formState.username,
email: formState.email,
password: formState.password,
password2: formState.password2,
};
// Post request to backend
axios
.post("http://localhost:4000/register", user)
.then((res) => {
// Redirect user to the /thankyouForRegistering page which prompts them to login.
history.push("/thankyouForRegistering");
})
.catch((err) => {
console.log(err);
});
}
But this does not work. I get an error back saying:
React Hook "useHistory" is called in function "handleRegister" which is neither a React function component or a custom React Hook function
Upon further research, it seems that in order to use the useHistory hook, it has to be within <Router>(possibly?) or directly on an onClick handler.
So something like this:
<Button onClick={() => history.push()}></button>
I can't really do that though, because I am not using onClick for my register button, I am using onSubmit and my own register function.
I also looked into using <Redirect />, so I tried making a new state called authorized, set the authorize state to true in my axios request, and then tried this:
<Route
path="/thankyouForRegistering"
render={() => (
authorized ? (
<ThankyouForRegistering />
) : (
<Redirect to="/register" />
))
}
/>
But this is not working either, and it also does not give me any kind of error.
Does anyone know the best way to redirect a user to a new page upon registering/logging in? I've been struggling with this for going on two weeks.
Thanks!!
EDIT: Here is the entire component - it's a bit messy but if anyone needs any explanations please let me know.
import React, { useState } from "react";
import {
BrowserRouter as Router,
Switch,
Route,
Redirect,
useHistory,
} from "react-router-dom";
let navMenu;
function App() {
let history = useHistory();
const [navMenuOpen, setNavMenuOpen] = useState(false);
const [loggedIn, setLoggedIn] = useState(false);
const [errorMsg, setErrorMsg] = useState("");
const [token, setToken] = useState("");
const [authorized, setAuthorized] = useState(false);
const initialState = {
username: "",
email: "",
password: "",
password2: "",
};
const [formState, setFormState] = useState(initialState);
const { username, email, password, password2 } = formState;
const handleChange = (e) => {
setFormState({ ...formState, [e.target.name]: e.target.value });
};
function handleRegister(e) {
//const history = useHistory();
e.preventDefault();
// Grab setState
const user = {
username: formState.username,
email: formState.email,
password: formState.password,
password2: formState.password2,
};
// Post request to backend
axios
.post("http://localhost:4000/register", user)
.then((res) => {
console.log(user);
history.push("/thankyouForRegistering");
setAuthorized(true);
// Redirect user to the /thankyouForRegistering page which prompts them to login.
})
.catch((err) => {
console.log(err);
});
// Once a user has registered, clear the registration form and redirect the user to a page that says thank you for registering, please login.
}
const handleLogin = (e) => {
e.preventDefault();
// Grab setState
const userData = {
email: formState.email,
password: formState.password,
};
axios
.post("http://localhost:4000/login", userData)
.then((res) => {
// Get token from local storage if there is a token
localStorage.setItem("token", res.data.token);
// If there is a token, redirect user to their profile and give them access to
// their recipeList and shoppingList
setLoggedIn(true);
//props.history.push("/profile");
})
.catch((err) => console.log(err));
};
const navMenuToggle = () => {
console.log("toggle");
setNavMenuOpen(!navMenuOpen);
};
const navMenuClose = () => {
setNavMenuOpen(false);
};
const logoutFromNavMenu = () => {
setLoggedIn(false);
navMenuClose();
};
return (
<Router>
<div>
<Navbar
loggedIn={loggedIn}
navMenuToggle={navMenuToggle}
/>
<NavMenu
loggedIn={loggedIn}
show={navMenuOpen}
navMenuClose={navMenuClose}
logoutFromNavMenu={logoutFromNavMenu}
/>
<Switch>
<Route
path="/login"
render={(props) => (
<Login
handleLogin={handleLogin}
handleChange={handleChange}
email={email}
password={password}
errorMsg={errorMsg}
/>
)}
/>
<Route
path="/register"
render={(props) => (
<Register
handleRegister={handleRegister}
handleChange={handleChange}
email={email}
username={username}
password={password}
password2={password2}
errorMsg={errorMsg}
/>
)}
/>
<Route
path="/profile"
render={() => (loggedIn ? <Profile /> : <Redirect to="/login" />)}
/>
<Route
path="/thankyouForRegistering"
render={() =>
authorized ? (
<ThankyouForRegistering />
) : (
<Redirect to="/register" />
)
}
/>
<Route
path="/recipes"
render={(props) =>
loggedIn ? <RecipeBook /> : <Redirect to="/login" />
}
/>
<Route
path="/list"
render={(props) =>
loggedIn ? <ShoppingList /> : <Redirect to="/login" />
}
/>
<Route path="/about" component={About} />
<Route path="/contact" component={Contact} />
<Route
path="/accountSettings"
render={(props) =>
loggedIn ? <AccountSettings /> : <Redirect to="/login" />
}
/>
<Route
exact
path="/"
component={() => <Home isLoggedIn={loggedIn} />}
/>
</Switch>
</div>
</Router>
);
}
export default App;

I figured out what the problem was. It wasn't enough to be using useHistory, I had to be using withRouter to get access to the history prop.
So I imported withRouter to my App component:
import { withRouter } from 'react-router-dom';
Then added this at the very bottom of the page, right below the App function:
const AppWithRouter = withRouter(App);
export default AppWithRouter;
So the App now must be exported as an argument of withRouter to get access to the history prop (as well as location and params I think?)
But this still wasn't enough. It was giving me an error saying that withRouter can only be used within <Router />(or <BrowserRouter />, whichever one you are using). So I wrapped my main <App /> in index.js with <BrowserRouter />
ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById("root")
);
And imported it of course.
This still wasn't enough, though. After adding
history.push("/thankYouForRegistering);
to my axios request, it added the route to the URL, but it did not re render the view. It was still stuck on the register page. Then I realized I had <BrowserRouter /> in two different places now. In my App component, I still had it wrapped around all of my routes, so after removing that and just returning a div with <Switch /> wrapped around all of my routes, that made it work like expected.
I hope this helps somebody who is facing the same issue!

Related

Maintain context values between routes in React.js

I'm trying to maintain state values between routes in context. But it gets reset when the route changes.
aackage.json:
"react-router-dom": "^6.8.0",
"react-dom": "^18.2.0",
"react": "^18.2.0",
App.js:
export default const App = () => {
const [loading, setLoading] = useState(false);
const [data, setData] = useState(null);
const getData = async () => {
setLoading(true);
const data = await axios.get("url", {
withCredentials: true,
});
setData(data);
setLoading(false);
};
useEffect(() => {
getData()
console.log("I run on route change");
}, []);
const GlobalContextValue= {
data: data,
loading: loading,
};
return (
<>
<GlobalContextProvider value={GlobalContextValue}>
<BrowserRouter>
<Routes>
<Route index element={<HomePage />} />
<Route path="/:slug" element={<PostPage />} />
{/* <Route path="*" element={<NoPage />} /> */}
</Routes>
</BrowserRouter>
</<GlobalContextProvider />
</>
)
}
Whenever I try to access any route the getData function inside the useEffect calls which inturns resets the data. I have attached a CodeSandbox to replicate the same
I don't know if this problem is related to reactJs or react-router. Thanks in advance
As you don't seem to have any navigation link, I assume you are using the browser search bar, or a normal HTML <a> tag. Well, doing so refreshes the page, so the entire app gets re-created.
Using useNavigate or Link from React Router Dom, doesn't refresh the page, hence your context data remains untouched:
const HomePage = () => {
return (
<>
<h1>Hii Homepage </h1>
<Link to="/1">Go to PostPage</Link>
</>
);
};
const PostPage = () => {
const params = useParams();
return (
<>
<h1>Hii PostPage {params.slug} </h1>
<Link to="/">Go to HomePage</Link>
</>
);
};
export default function App() {
useEffect(() => {
console.log(
"I run on load and route change with browser search bar, not with useNavigate or Link"
);
}, []);
return (
<>
{/* This context wrapping BrowserRouter keeps its value if you navigate with Link or
useNavigate. */}
<GlobalContextProvider value={{ key: "some value" }}>
<BrowserRouter>
<Routes>
<Route index element={<HomePage />} />
<Route path="/:slug" element={<PostPage />} />
{/* <Route path="*" element={<NoPage />} /> */}
</Routes>
</BrowserRouter>
</GlobalContextProvider>
</>
);
}

Redirect user after authorization where user previously clicked

Somehow I came to a problem of thinking of how to make that if the person clicks on a link, it should be redirected to sign-in page if not authorized and to that page if authorized. This sounds easy, but the problem is that I want to make that if the user redirected to one page where it should be authorized, the user authorizes and redirect to the same page as he clicked.
For now, I have a protected route that looks like this: (I have fromPath argument for next redirection but that does not work for me.)
const ProtectedRoute = ({
isAllowed,
redirectPath = "/sign-in",
fromPath = null,
children,
}) => {
const dispatch = useDispatch();
if (fromPath) dispatch(setURLPath(fromPath));
if (!isAllowed) {
return <Navigate to={fromPath} replace />;
}
return children ? children : <Outlet />;
};
And here how it looks from the App.js side:
<Suspense fallback={<Spinner />}>
<GlobalStyle />
<Routes>
<Route
path='/'
element={
<ProtectedRoute
isAllowed={roleLevel > 0}
/>
}
>
<Route path='bookings' element={<BookingsPage />} />
<Route path='single-booking/:id' element={<SingleBookingPage />} />
<Route path='documents' element={<DocumentsPage />} />
<Route path='my-account' element={<MyAccountPage />} />
<Route path='reservation' element={<ReservationPage />} />
</Route>
</Route>
<Route path='*' element={<NotFoundPage />} />
</Routes>
</Suspense>
The ProtectedRoute component should grab the current location object for the route being accessed and pass this in route state to the login route.
import { useLocation } from 'react-router-dom';
const ProtectedRoute = ({
isAllowed,
redirectPath = "/sign-in",
fromPath = null,
children,
}) => {
const location = useLocation();
const dispatch = useDispatch();
if (fromPath) dispatch(setURLPath(fromPath));
if (!isAllowed) {
return <Navigate to={fromPath} replace state={{ from: location }} />;
}
return children ? children : <Outlet />;
};
The login component should then access the passed route state and redirect back to the original route being accessed.
const location = useLocation();
const navigate = useNavigate();
...
const login = () => {
...
const { from } = location.state || { from: { pathname: "/" } };
navigate(from, { replace: true });
};
You can achieve this by passing some params (next_route) for example. and keep it along the process in signin so that when he finishes he can ge reredirected to the right place (next_route)

React context api lose auth data when react router dom push page

I have an context where i save the user data, and i have another component when verify the context user is null, if the context user is null my component should redirect the user to the login page, if not should render the component. My routers is inside my Authprovider, but still losing the user data when reload the router. I found another posts with the same issue, and the instruction is to keep the routers inside the useauthprovider, but doesn't work with my app.
My code
function App() {
let header = window.location.pathname === '/login' || '/cadastro' ? <Header /> : null;
let footer = window.location.pathname === '/login' || '/cadastro' ? <Footer /> : null;
return (
<UseAuthProvider> // My use AuthProvider
<Router>
<div className='app-container' >
<Switch>
<Cart>
<Header />
<NavbarMenu />
<div className='app-body'>
<UseCampanhaProvider>
<PublicRoute exact path='/' component={Home} />
<PrivateRoute exact path='/cupom/:campaignId' component={CupomScreen} />
<PrivateRoute exact path='/carrinho' component={CartScreen} />
</UseCampanhaProvider>
<PublicRoute exact path='/login' restricted={true} component={Login} />
<PublicRoute path='/cadastro' restricted={true} component={Cadastro} />
</div>
<AuthModal />
{footer}
</Cart>
</Switch>
</div>
</Router >
</UseAuthProvider>
);
}
export default App;
My component where i verify the user context
const PrivateRoute = ({ component: Component, ...rest }) => {
const { user } = useAuth();
return (
<Route {...rest} render={props => (
!user ?
<Redirect to='/login' />
:
<Component {...props} />
)} />
);
};
export default PrivateRoute;
My context where i load the user
const UseAuthProvider = ({ children }) => {
const [user, setUser] = useState();
const [open, setOpen] = useState(false)
useEffect(() => {
verifyUser(); //here i call the function when verify the localstorage
}, [])
const verifyUser = async () => {
let tokenHeader = authHeader();
if (tokenHeader) {
await Api.post('/cliente/index', {}, {
headers: {
...tokenHeader
}
}).then((response) => {
setUser(response.data.cliente)
})
}
}
const handleModal = () => {
setOpen((state) => !state)
}
const Logout = async () => {
localStorage.clear('acessToken-bolao')
setUser(null)
}
return (
<useAuthContext.Provider value={{ Auth, verifyUser, user, Register, Logout, open, handleModal }}>
{children}
</useAuthContext.Provider>
)
}
I tried to debug my application and when i redirect my user to another router, before the component render my user return undefined, and after my component is rendered the context load the user data.
It sounds like your entire application is unmounting and remounting.
In this case the state will be lost as it is not simply a re-render.
By what mechanism are you navigating to the new page?
If I remember React-Router correctly you need to use
If you try navigating the url itself with window.location or href then you are reloading the entire page (not using the router in the SPA)
If routed correctly I would expect that only data inside the Switch would be re-loaded.

How to /WAIT A SECOND/ to navigate after user signed in in a restricted route?

So what I mean is, I have a functionality of after user signed in, navigate him/her to the homepage in a second, such as:
const handleSignIn = async (e) => {
e.preventDefault();
const user = await signIn(formData);
if (user) {
toast.success('Signed In!');
setTimeout(() => {
navigate('/');
}, 1000);
} else {
toast.error('Bad user credentials!');
}
};
But, I built a protected route, such as:
function App() {
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (user) => {
setIsLoading(true);
if (user) {
setUser(user);
} else {
setUser(null);
}
setIsLoading(false);
});
return unsubscribe;
}, []);
if (isLoading) return <Spinner />;
return (
<>
<Router>
<Routes>
<Route path='/' element={<PrivateRoute />}>
<Route index element={<Explore />} />
<Route path='/offers' element={<Offers />} />
<Route path='/profile' element={<Profile />} />
<Route path='/contact/:landlordId' element={<Contact />} />
<Route path='/create-listing' element={<CreateListing />} />
<Route path='/category/:categoryName' element={<Category />} />
<Route path='/category/:categoryName/:id' element={<Listing />} />
</Route>
<Route
path='/sign-in'
element={!user ? <SignIn /> : <Navigate to='/' />}
/>
<Route
path='/sign-up'
element={!user ? <SignUp /> : <Navigate to='/' />}
/>
<Route
path='/forgot-password'
element={!user ? <ForgotPassword /> : <Navigate to='/' />}
/>
</Routes>
{user && <Navbar />}
</Router>
<ToastContainer autoClose={1000} />
</>
);
}
export default App;
So the problem here is, whenever user signs in or signs out,the onAuthStateChanged gets executed, therefore it enforce app to re-render in order to understand who's logged in and logged out then behave accordingly, like restrict some routes and allow others.
When user signs in, I'd like to show some toast message BUT it triggers the App level state and before I show toast and navigate user, it automatically navigates user into the homepage due to the restriction, how can I wait a second before doing that?
Can't you just wait for x amount of time before calling setUser()? That is because setUser() updates the user value, which triggers navigation.
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (user) => {
setIsLoading(true);
setTimeout(() => {
if (user) {
setUser(user);
} else {
setUser(null);
}
setIsLoading(false);
}, 1000);
});
return unsubscribe;
}, []);
Alternatively, use async/await with a custom wait async function that will avoid nesting:
const wait = async (d) => new Promise(r => setTimeout(d, r));
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, async (user) => {
setIsLoading(true);
await wait(1000);
if (user) {
setUser(user);
} else {
setUser(null);
}
setIsLoading(false);
});
return unsubscribe;
}, []);
Allow the SignIn, component to handle the redirect itself. You have two different pieces of logic that are redirecting to "/"
Instead of this:
<Route
path='/sign-in'
element={!user ? <SignIn /> : <Navigate to='/' />}
/>
Switch to this:
<Route
path='/sign-in'
element={<SignIn />}
/>
If that causes a problem (ie, if you want to bypass SignIn and go right to / if the user is already logged in), you should add a useEffect call to SignIn and call navigate('/'); there if necessary.

Can't pass data from one page to another with React-Router-DOM (useHistory, useLocation)

I have a router:
<Switch>
<Route exact path="/">
<CustomPaddingContainer padding="0 0.5em 1.5em">
<TableViewComponent columns={tableAccessors} />
</CustomPaddingContainer>
</Route>
<Route path="/new-objective">
<AddNewObjectiveComponent onSubmit={onSubmitObjective} onCancel={onCancel} />
</Route>
<Route path="/new-kr">
<AddNewKrComponent onSubmit={onSubmitKR} onCancel={onCancel} />
</Route>
<Route path="/okr-details/:id">
<OkrDetailsWithParams />
</Route>
</Switch>
and I want to pass specific data from specific component to one of this Route when specific button will be clicked. to be more precise, I have this component:
const AddOKRButtons: FC<AddOKRButtonsProps> = ({ parentObjectiveId }) => {
const history = useHistory();
const onAddOkrButtonClick = () => {
history.push('/new-objective', { parentObjectiveId: parentObjectiveId });
};
const onAddKrButtonClick = () => {
history.push('/new-kr', { parentObjectiveId: parentObjectiveId });
};
return (
<OkrDetailsChildrenCardsButtonContainerCentered>
<ButtonGroup>
<LinkButton to="/new-objective" appearance="default" onClick={onAddOkrButtonClick}>
Add a new sub-objective
</LinkButton>
<LinkButton to="/new-kr" appearance="default" onClick={onAddKrButtonClick}>
Add a new key-result
</LinkButton>
</ButtonGroup>
</OkrDetailsChildrenCardsButtonContainerCentered>
);
};
Im trying to pass the **parentObjectiveId** which is coming from props to the /new-objective page or /new-kr page in order what button was clicked. After that Im trying to get that data in component where it should be with useLocation hook:
export const AddNewObjectiveComponent: FC<NonNullable<AddNewOKRProps>> = props => {
const location = useLocation();
console.log(location);
return(<div></div>)
}
and unfortunately i got undefined in the state key, where the data is probably should be:
Try to push history route like
history.push({
pathname: '/new-objective',
state: { parentObjectiveId: parentObjectiveId }
});
I hope it will be work for you. Thanks!

Categories

Resources