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
Related
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;
};
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 am using react-redux to manage state and axios to make API calls.
My default state:
const defaultState = {
isLoading: true,
isAuthenticated: false,
authUser: {}
}
isLoading and authUser are both updated once the API's login call has ran successfully. Nothing wrong here.
In a userProfile component, I am using the following to get the logged in user's ID from the store:
const authUser = useSelector(state => state.authentication.authUser);
const user = users.getUser(authUser.id);
console.log(authUser);
authUser is always empty on page load but a split second late, it prints again, this time with the state contents.
I have the userProfile component behind a custom PrivateRoute component:
const PrivateRoute = ({ component: Component, ...rest }) => {
const authentication = useSelector(state => state.authentication);
return (
<Route
{...rest}
render={props =>
authentication.isAuthenticated ? (
<Component {...props} />
) : (
authentication.isLoading ? 'loading...' :
<Redirect to={{ pathname: '/login', state: { from: props.location } }} />
)
}
/>
)
};
The loading... text shows before the component catches up, then the 2 console logs appear one after the other. The first empty, the second with data.
What am I missing here? This async stuff is driving me insane!
with this implementation of private route u can save current path of user and let user continue his process after login again
import React from 'react';
import { Login } from '../../pageComponents';
import { useSelector } from 'react-redux';
const PrivateRoute = (Component) => {
const Auth = (props) => {
const { isLoggedIn } = useSelector((state) => state.user);
// Login data added to props via redux-store (or use react context for example)
// If user is not logged in, return login componentc
if (!isLoggedIn) {
return <Login />;
}
// If user is logged in, return original component
return <Component {...props} />;
};
// Copy getInitial props so it will run as well
if (Component.getInitialProps) {
Auth.getInitialProps = Component.getInitialProps;
}
return Auth;
};
export default PrivateRoute;
then user it every page you want Private Rout like below
const myPrivateRoute = () => {
return (
<h1>Hello world</h1>
)
}
export default PrivateRoute(myPrivateRoute)
so create component like this and load your last section from local storage or cookie then validate and send into your redux store then you can memorize log
import { useEffect, useState } from "react";
import { useSelector, useDispatch } from "react-redux";
import axios from "axios";
import * as globalActions from "../../../store/actions/globalAction";
import * as userAction from "../../../store/actions/userAction";
const StorageManagment = () => {
const dispatch = useDispatch();
/* ----------------------------------- listening to token changes ----------------------------------- */
useEffect(() => {
dispatch(userAction.loadCart());
}, []);
useEffect(async () => {
try {
if (window)
await checkLocalDataWithRedux()
.then((data) => {
const { userDTO, token } = data;
dispatch(userAction.loginUser(token, userDTO));
})
.catch((e) => {
console.log(e);
dispatch(userAction.logOutUser());
});
} catch (e) {
throw e;
}
}, []);
function checkLocalDataWithRedux() {
return new Promise((resolve, reject) => {
try {
/* -------- read user data from localStorage and set into redux store ------- */
const { userDTO, token } = localStorage;
if (userDTO && token) {
resolve({ token: token, userDTO: JSON.parse(userDTO) });
} else logOutUser();
} catch (e) {
reject();
}
});
}
function logOutUser() {
dispatch(userAction.logOutUser());
}
return null;
};
export default StorageManagment;
then load it in your app.jsx file
app.js
render() {
return (
<React.Fragment>
<Provider store={store}>
<ErrorBoundary>
<StorageManagment />
<DefaultLayout>
<Application {...this.props} />
</DefaultLayout>
</ErrorBoundary>
</Provider>
</React.Fragment>
);
}
I've implement a simple protected route from Login & other protected components.
I'm using universal-cookie to persist user data/token
On protected route component, I make sure to always check whether the user exist or cookie exist, thus user is LOGIN. Otherwise, no. This is done with Context API
Protected Route for Login.js
this protected route is here to make sure if user cookie exist or user authenticate, then user should be redirect to Dashboard.js (home page for logged-in users)
ProtectedRouteLogin.js:-
import React, { useEffect } from 'react'
import { Redirect, Route } from 'react-router-dom'
import { useAuth } from '../../../contexts/Auth/AuthState'
import { isAuthenticated, setLoading } from '../../../contexts/Auth/AuthAction'
const ProtectedRoute\Login = ({ component: Component, ...rest }) => {
const [authState, authDispatch] = useAuth()
const { authenticated, loading } = authState
// check if user is authenticated
useEffect(() => {
(async() => {
await isAuthenticated(authDispatch)
setLoading(authDispatch, false)
})();
}, [])
return (
<Route
{...rest} render={
props => {
if(loading) return <div className="lds-hourglass"></div>
if(!authenticated) return <Component {...props} />
else return <Redirect exact to={{
// here I redirect logged-in user to dashboard
pathname: "/dashboard",
state: { from: props.location }
}} />
}
}
/>
)
}
export default ProtectedRouteLogin
Protected Route for Dashboard.js or other protected components
this exist for the same purpose as before, checking whether user is authenticated or not. If YES, then render the protected component. Otherwise, redirect to Login page
ProtectedRouteOthers.js:-
import React, { useEffect } from 'react'
import { Redirect, Route } from 'react-router-dom'
import { useAuth } from '../../../contexts/Auth/AuthState'
import { isAuthenticated, setLoading } from '../../../contexts/Auth/AuthAction'
const ProtectedRouteOthers = ({ component: Component, ...rest }) => {
const [authState, authDispatch] = useAuth()
const { authenticated, loading } = authState
// check if user is authenticated
useEffect(() => {
(async() => {
await isAuthenticated(authDispatch)
setLoading(authDispatch, false)
})();
}, [])
return (
<Route
{...rest} render={
props => {
if(loading) return <div className="lds-hourglass"></div>
if(authenticated) return <Component {...props} />
else return <Redirect to={
{
// here redirect NOT logged-in user back to login page
pathname: "/login",
state: { from: props.location }
}
} />
}
}
/>
)
}
export default ProtectedRouteOthers
Issues facing
Since the checking (user aunthentication) is done in protected route layer or component just before rendering any protected routes, I'm having an issue where whenever I refreshed page (when logged-in). It keeps bring me back to Dashboard.js page.
I reckoned, this have something todo with having my authenticated state in Context API is false by default. Every time, I hit refreshed, the authenticated state will be false first in default state until I change/update it, after checking user cookie exist or not. (this is done in isAunthenticated function in Context API which was called in both Protected Routed above)
isAuthenticated function (check if user have cookie or not)
export const isAuthenticated = async(dispatch) => {
setLoading(dispatch, true)
// get user cookie
let userExist = getCookie('user')
/** Cookie Checking Style */
if(userExist) {
dispatch({
type: 'SET_ISAUTHENTICATED',
payload: true
})
} else {
dispatch({
type: 'SET_ISAUTHENTICATED',
payload: false
})
}
}
So how can I alter my code so that, every time i refresh my protected page (when logged-in). I will stay on the refreshed page, instead of going back to the Dashboard page?
Luckily I found a solution. But I don't know whether this is an acceptable solution or not. At least this logic make sense to overcome my issue.
I decided to add a logic before rendering Login.js page & other protected components in both Protected Route components (logic added in ProtectedRouteLogin.js & ProtectedRouteOthers)
Logic added in ProtectedRouteLogin.js
here before rendering to Dashboard.js page, if user is authenticated, I check for props.location.pathname saved in cookie first. If present/exist, then render to said pathname. Otherwise, just render to Dashboard.js page
ProtectedRouteLogin.js:-
import React, { useEffect } from 'react'
import { Redirect, Route } from 'react-router-dom'
import { useAuth } from '../../../contexts/Auth/AuthState'
import { isAuthenticated, setLoading } from '../../../contexts/Auth/AuthAction'
import { getCookie } from '../../../services/Cookie'
const ProtectedRouteLogin = ({ component: Component, ...rest }) => {
const [authState, authDispatch] = useAuth()
const { authenticated, loading } = authState
// check if user is authenticated
useEffect(() => {
(async() => {
await isAuthenticated(authDispatch)
setLoading(authDispatch, false)
})();
}, [])
return (
<Route
{...rest} render={
props => {
if(loading) return <div className="lds-hourglass"></div>
if(!authenticated) return <Component {...props} />
else {
// get pathname save in cookie
let url = getCookie('onRefresh')
return <Redirect exact to={{
// ternary function to determine redirection
pathname: url !== '' ? url : '/pfv4-admin/dashboard',
state: { from: props.location }
}} />
}
}
}
/>
)
}
export default ProtectedRouteLogin
Logic added in ProtectedRouteOthers.js
here, before rendering to any protected components, I set cookie that gonna hold the props.location.pathname or url of about-to visit component. So that, I can cross-check this url in ProtectedRouteLogin.js above.
ProtectedRouteOthers.js:-
import React, { useEffect } from 'react'
import { Redirect, Route } from 'react-router-dom'
import { useAuth } from '../../../contexts/Auth/AuthState'
import { isAuthenticated, setLoading } from '../../../contexts/Auth/AuthAction'
import { setCookie } from '../../../services/Cookie'
const ProtectedRouteOthers = ({ component: Component, ...rest }) => {
const [authState, authDispatch] = useAuth()
const { authenticated, loading } = authState
// check if user is authenticated
useEffect(() => {
(async() => {
await isAuthenticated(authDispatch)
setLoading(authDispatch, false)
})();
}, [])
return (
<Route
{...rest} render={
props => {
if(loading) return <div className="lds-hourglass"></div>
if(authenticated) {
// save last page seen address (url)
setCookie('onRefresh', props.location.pathname, { path: '/' })
return <Component {...props} />
}
else return <Redirect to={
{
pathname: "/pfv4-admin",
state: { from: props.location }
}
} />
}
}
/>
)
}
export default ProtectedRouteOthers
Feel free to add your own possible solution for this issue tho. Looking forward to try them.
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
}