What is the difference between Navigate component and navigate from useNavigate() hook?.
if (therapy !== desiredGroupTherapy || required.includes('admin') && !isAdmin) {
const pathname = generatePath(pageRoutes.DASHBOARD, {
groupId: desiredGroupId,
therapy: therapyForValidGroup,
})
navigate(pathname, { replace: true })
// return <Navigate to={pathname} replace />
}
I have issue with navigate here. How that should work: I redirect to that page, but at first time I have no therapy, so I should redirect to the same page but my therapy now will be equal to therapyForValidGroup. I have custom hook useTherapy() which takes therapy from URL. So at first time when it is undefined it crashes using navigate function. But using Navigate component, it works fine. So what is the difference?
The navigate function, when invoked, is a side-effect. The code is calling navigate directly in the body of the component as an unintentional side-effect.
Move the logic into a useEffect hook.
Example:
import React, { useEffect } from "react";
import { generatePath, useParams } from "react-router";
import { useNavigate } from "react-router-dom";
import { usePathPattern } from "./usePathPattern";
export const Authorization = ({ children }) => {
const params = useParams();
const navigate = useNavigate();
const userTherapy = params.therapy;
const name = params.name;
const pattern = usePathPattern();
useEffect(() => {
if (userTherapy === "undefined" && name) {
const pathname = generatePath(pattern, {
name,
therapy: "TEST"
});
navigate(pathname, { replace: true });
}
}, [name, navigate, pattern, userTherapy]);
return children;
};
Related
React Beginner here. I'm building a login form in React using jwt, axios and useContext. After being authorized from the backend, I store the data in the global context using AuthProvider and redirect to home page. the home page first checks for authorization and navigates to login on unauthorized access. However even after updating the auth (useState) on login, I still get a false value on the first click and get sent back to login even if authed. I've tried useEffects everywhere but to no avail. Code below
AuthProvider.jsx
import React, { useState } from "react";
import { createContext } from "react";
const AuthContext = createContext();
export default AuthContext
export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [authed, setAuthed] = useState(false);
function login(user) {
setUser(user);
setAuthed(true);
}
return (
<AuthContext.Provider value={{login, user, authed}}>
{children}
</AuthContext.Provider>
)
}
ProtectedRoute.jsx to /home
import React, { useContext, useEffect } from "react";
import AuthContext from "../../context/AuthProvide";
const ProtectedRoute = ({children}) => {
const {login, user, authed} = useContext(AuthContext);
useEffect(() => {
alert("HELLO")
alert(authed)
if (!authed) {
return window.location.href = "/login";
} else {
return children
}
}, [authed, user, login]);
}
export default ProtectedRoute;
Top part of Login.jsx
import React, { useState, useEffect, useRef, useContext } from "react";
import "./login.css";
import AuthContext from "../../context/AuthProvide";
import { axios } from "../../context/axios";
const LOGIN = "/login";
const Login = () => {
const {login, user, authed} = useContext(AuthContext);
const [userEmail, setUserEmail] = useState("");
const [userPassword, setUserPassword] = useState("");
const [error, setError] = useState("");
const errorRef = useRef();
useEffect(() => {
}, [authed, user, login]);
useEffect(() => {
setError("");
}, [userEmail, userPassword]);
function handleUserEmail(event) {
setUserEmail(event.target.value);
}
function handleUserPassword(event) {
setUserPassword(event.target.value);
}
function handleSubmit(event) {
event.preventDefault();
axios.post(LOGIN, {
email: userEmail,
password: userPassword
}).then(response => {
if (response.data.error) {
setError(response.data.error);
} else {
// this is supposed to be the one to set the user and auth to true
login(response.data.token)
alert(authed)
window.location.href = "/";
}
}).catch(error => {
if (!error?.response) {
setError("NO SERVER RESPONSE");
} else if (error.response?.status === 400) {
setError("MISSING USER NAME OR PASSWORD");
} else if (error.response?.status === 401) {
setError("UNAUTHORIZED ACCESS");
} else {
setError("UNKNOWN ERROR");
}
errorRef.current.focus();
})
}
function resetForm() {
setUserEmail("");
setUserPassword("");
}
return (
// the form is here
)
Issues
The main issue I see with the code is the use of window.location.href. When this is used it reloads the page. This remounts the entire app and any React state will be lost/reset unless it is being persisted to localStorage and used to initialize app state.
It is more common to use the navigation tools from react-router-dom (I'm assuming this is the package being used, but the principle translates) to issue the imperative and declarative navigation actions.
Suggestions
The protected route component should either redirect to the login path or render the children prop if user is authorized. It passes the current location being accessed along in route state so user can be redirected back after successful authentication.
import { Navigate } from 'react-router-dom';
const ProtectedRoute = ({ children }) => {
const location = useLocation();
const { authed } = useContext(AuthContext);
if (!authed) {
return <Navigate to="/login" replace state={{ from: location }} />;
} else {
return children;
}
};
The Login component should use the useNavigate hook to use the navigate function to redirect user to protected route once authenticated.
import { useLocation, useNavigate } from 'react-router-dom';
const Login = () => {
const { state } = useLocation();
const navigate = useNavigate();
const { login, user, authed } = useContext(AuthContext);
...
function handleSubmit(event) {
event.preventDefault();
axios.post(
LOGIN,
{
email: userEmail,
password: userPassword
}
)
.then(response => {
if (response.data.error) {
setError(response.data.error);
} else {
login(response.data.token)
navigate(state?.from?.pathname ?? "/", { replace: true });
}
})
.catch(error => {
if (!error?.response) {
setError("NO SERVER RESPONSE");
} else if (error.response?.status === 400) {
setError("MISSING USER NAME OR PASSWORD");
} else if (error.response?.status === 401) {
setError("UNAUTHORIZED ACCESS");
} else {
setError("UNKNOWN ERROR");
}
errorRef.current.focus();
});
}
...
return (
// the form is here
);
}
Wrap the routes you want to protect with the ProtectedRoute component.
<AuthProvider>
<Routes>
...
<Route path="/login" element={<Login />} />
<Route
path="/test"
element={
<ProtectedRoute>
<h1>Protected Test Route</h1>
</ProtectedRoute>
}
/>
</Routes>
</AuthProvider>
I have a problem, watching a video on youtube that used history.push('/login?redirect=shipping'), how can I modify it using navigate, because if I use the string navigate ('/ login?redirect=shipping') even if logged in it returns to home page and does not go to the shipping page.
I solved the problem in this way :
const checkoutHandler = () => {
if(!userInfo){
navigate('/login')
} else{
navigate('/shipping')
}
}
Your solution is good.
Can I fill in some gaps?
We need to get the state of user to check if user logged in to proceed with if statement.
After this your solution will work.
import React from 'react'
import { useNavigate } from 'react-router-dom'
import { useSelector } from 'react-redux'
//....some imports
const CartScreen = () => {
//.......some code before
const navigate = useNavigate()
const userLogin = useSelector((state) => state.userLogin)
const { userInfo } = userLogin
const checkoutHandler = () => {
if (!userInfo) {
navigate('/login')
} else {
navigate('/shipping')
}
}
//...return JSX...
**Try this one.
const navigate = useNavigate()
const checkoutHandler = () => {
navigate('/signin?redirect=/shipping')
}
Have you sort out this problem or not? If you are facing the same problem then I suggest you go on your login page and make some changing.
Import useHistory and useLocation:
import { Link, useHistory, useLocation } from 'react-router-dom'
Then in functions apply this:
const history = useHistory();
const location = useLocation();
Im trying to redirect my user when the user is logged in. but all my methods i found so far wont work. etg im trying to use useNavigate function using react router v6.
but for some reason i get the following error:
Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
See react-invalid-hook-call for tips about how to debug and fix this problem.
at:
/login.jsx:35
let navigate = useNavigate();
function:
PerformLogin = () => {
let navigate = useNavigate();
const username = this.state.username;
const password = this.state.password;
if (username === '') {
console.log("please enter username");
} else if (password === '') {
console.log("please enter password");
} else {
console.log("username of login: ",this.state.username);
axios.post(process.env.REACT_APP_DATAURL + `/login`,{ username: username, password: password },{withCredentials: true})
.then(res => {
res = res.data;
console.log(res);
console.log(res.data);
if (res.type) {
console.log("navigating logged in");
navigate.push('/crime');
//return <Navigate to="/crime" />;
}
})
}
}
Q: what do i have to do to fix this and be able to redirect my user?
Edit: For react-router v6
In react-router v6 there is no withRouter nor useHistory. I would recommend to refactor your component to use hooks for the sake of simplicity, but an alternative solution is to create a wrapper component that will pass the navigate function obtained from the hook as a prop:
import { useNavigate } from 'react-router-dom';
class MyComponent extends React.Component {
//...
PerformLogin = () => {
const username = this.state.username;
const password = this.state.password;
// ...
this.props.navigate('/crime');
}
}
function WithNavigate(props) {
let navigate = useNavigate();
return <MyComponent {...props} navigate={navigate} />
}
export default WithNavigate
useHistory is a hook and hooks can only be used inside "function" components.
However I can guess you are using it from a "class" component, since there are some this.state, so you cannot use the hook there.
Another approach that should work for you would be to wrap your component inside a withRouter:
import { withRouter } from "react-router";
class MyComponent extends React.Component {
//...
PerformLogin = () => {
const history = this.props.history;
const username = this.state.username;
const password = this.state.password;
// ...
}
}
export default withRouter(MyComponent)
The withRouter will inject the history as a prop.
In react-router-dom latest version(v6) you cannot use this.props.match.params.id and this.props.history (in react latest vesion history replace with navigate). Besides you cannot call them in class component. To call them in class component make a function first -
import { useParams, useNavigate } from "react-router-dom";
export function withParamsAndNavigate(Component) {
return (props) => (
<Component {...props} params={useParams()} navigate={useNavigate()} />
);
}
I want to use it for ProductDetails component. That's given below -
import { withParamsAndNavigate } from "./getParamsAndNavigate.js";
class ProductDetails extends React.Component {
state = {//}
render() {
const{params,navigate} = this.props;
// params.id - will return the id,
// navigate('yourUrl') - will redirect the target url.
return (
//
);
}
}
export default withParamsAndNavigate(ProductDetails);
Here is my App module where I defined my routes -
import React from "react";
import { Routes, Route} from "react-router-dom";
import Product from "./components/Product";
import ProductDetails "./components/ProductDetails";
function App() {
return (
<div>
<main role='main' className='container'>
<Routes>
<Route path='/product' element={<Product />} />
<Route path="/product/:id"} element={<ProductDetails />} />
</Routes>
</main>
</div>
);
}
export default App;
It works for me and hope it will help you.
You can only call hooks at the top level (https://reactjs.org/docs/hooks-rules.html) inside a component.
Therefore, this is invalid, being it's not only in an if statement, in a promise's .then function, and also does not appear to even be a functional component, rather, just a function:
if (res.type) {
console.log("navigating logged in");
navigate.push('/crime');
//return <Navigate to="/crime" />;
}
I would instead attempt doing something like setting a flag, and then navigating based upon that flag:
PerformLogin = () => {
let navigate = useNavigate();
let shouldNavigate = false;
const username = this.state.username;
const password = this.state.password;
if (username === '') {
console.log("please enter username");
} else if (password === '') {
console.log("please enter password");
} else {
console.log("username of login: ",this.state.username);
axios.post(process.env.REACT_APP_DATAURL + `/login`,{ username: username, password: password },{withCredentials: true})
.then(res => {
res = res.data;
console.log(res);
console.log(res.data);
if (res.type) {
console.log("navigating logged in");
shouldNavigate = true;
//return <Navigate to="/crime" />;
}
})
}
navigate.push(shouldNavigate ? '/crime' : currentURL);
}
But I would avoid this altogether and simply utilize the useHistory hook to get an instance of history and work on that (I haven't tested this either but it should work):
PerformLogin = () => {
const history = useHistory();
const username = this.state.username;
const password = this.state.password;
if (username === '') {
console.log("please enter username");
} else if (password === '') {
console.log("please enter password");
} else {
console.log("username of login: ",this.state.username);
axios.post(process.env.REACT_APP_DATAURL + `/login`,{ username: username, password: password },{withCredentials: true})
.then(res => {
res = res.data;
console.log(res);
console.log(res.data);
if (res.type) {
console.log("navigating logged in");
history.push('/crime');
//return <Navigate to="/crime" />;
}
})
}
}
Note: You have to do this directly in your component itself, you can't use hooks in a normal function that isn't a component.
*An example of how you can use.useHsitory has not been working properly in v6
import React from 'react';
import { createBrowserHistory } from 'history';
const StatusCodeCat: React.FC = () => {
// const history = useHistory();
const history = createBrowserHistory();
console.log(history);
return (
<div>
never
</div>
);
}
export default StatusCodeCat;
you don't use useNavigate() in ordinary function
import React from "react"
import { useSelector } from 'react-redux'
import { useLocation } from "react-router"
const BreadCrumb = () => {
const location = useLocation()
const currentPageTitle = useSelector(state => {
// match current location to get the currrent page title from redux store and return it
})
return (
<h2>Home / { currentPageTitle}</h2>
)
}
export default BreadCrumb
This code works fine in the initial render and I do get the intended result { currentPageTitle } but the UI doesn't seem to re-render and stays the same despite route change. Although, if I console.log( location ) before the return statement, it logs successfully on route change. What is the issue here?
I would suggest that you use useEffect hook:
import React, { useEffect, useState } from "react"
import { useSelector } from 'react-redux'
import { useLocation } from "react-router"
const BreadCrumb = () => {
const location = useLocation()
const [currentLocation, setCurrentLocation] = useState('')
const currentPageTitle = useSelector(state => {
console.log(currentLocation);
// Do something with currentLocation
})
useEffect(() => {
if (location) {
setCurrentLocation(location.pathname)
} else {
setCurrentLocation('')
}
}, [location])
return (
<h2>Home / { currentPageTitle}</h2>
)
}
export default BreadCrumb
So I'm creating a loader component that requests data to a server, then based on the response - then the loader page redirects/reroutes them to other pages.
But for some reason history.push changes the route but does not render the component and remains with the <LoaderPage> component. Not sure what am I missing. So kindly help.
I have wrapped all pages with the <LoaderPage> component as my goal is when every time a user visits any route the <LoaderPage> component renders first then it does its job then redirects/reroutes users to other pages.
loader.tsx
import React from 'react';
import { useHistory } from 'react-router-dom'
import { AnimatedLodingScreen } from './loaders/AnimatedLodingScreen'
type loaderProps = {
children: React.ReactNode;
};
export const LoaderPage = ({ children }:loaderProps) => {
const history = useHistory();
React.useEffect(() => {
const session = http.get('/some-route');
session.then((result) => {
if(result.authenticated) {
history.push('home'); // when user is logged
}
history.push('/login'); // not authenticated
}
}, [])
return (
<AnimatedLodingScreen/>
);
}
app.tsx
import React from 'react';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import { observer } from 'mobx-react';
import { LoaderPage } from 'pages/loaders/LoaderPage';
import { SomeComponent1, SomeComponent2 } from 'pages/index'
export const App: React.FC = observer(() => {
return (
<BrowserRouter>
<LoaderPage>
<Route path='/home' exact component={SomeComponent1}/>
<Route path='/login' exact component={SomeComponent2}/>
// and so on... I have alot of routes in fact these routes are looped via .map and
// type-checked i just put it like this for simplicity
</LoaderPage>
</BrowserRouter>
);
});
index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import { App } from 'app/app';
ReactDOM.render(<App/>, document.getElementById('root'));
The children props taken by your LoaderPage component isn't used for render anywhere within it, and thus nothing is rendered.
export const LoaderPage = ({ children }:loaderProps) => {
const history = useHistory();
React.useEffect(() => {
const session = http.get('/some-route');
session.then((result) => {
if(result.authenticated) {
history.push('home'); // when user is logged
}
history.push('/login'); // not authenticated
}
}, [])
return (
{children || <AnimatedLodingScreen/>}
);
}
You can use a state to save whether the data loading is completed or not and render children based on that
export const LoaderPage = ({ children }:loaderProps) => {
const history = useHistory();
const [isLoaded, setLoaded] = React.useState(false)
const [redirectPath, setRedirectPath] = React.useState('')
React.useEffect(() => {
const session = http.get('/some-route');
session.then((result) => {
if(result.authenticated) {
return setRedirectPath('/home') // when user is logged
}
setRedirectPath('/login') // not authenticated
}
}, [])
function redirectToPath() {
setLoaded(true);
history.push(redirectPath)
}
if(isLoaded) { return <>{children}</> }
return <AnimatedLodingScreen onAnimationEnd={redirectToPath} /> // onAnimationEnd is the function passed as prop to the component that should be invoked on animation ends
}