Navigate to different page on successful login - reactjs

App.tsx
import React from 'react';
import {
BrowserRouter as Router,
Redirect,
Route,
Switch
} from 'react-router-dom';
import './app.css';
import Login from './auth/pages/login';
import DashBoard from './dashboard/dashboard';
export const App = () => {
return (
<div className="app">
<Router>
<Switch>
<Route path="/auth/login" component={Login} />
<Route path="/dashboard" component={DashBoard} />
<Redirect from="/" exact to="/auth/login" />
</Switch>
</Router>
</div>
);
};
export default App;
login.tsx
import { useHistory } from 'react-router-dom';
const authHandler = async (email, password) => {
const history = useHistory();
try {
const authService = new AuthService();
await authService
.login({
email,
password
})
.then(() => {
history.push('/dashboard');
});
} catch (err) {
console.log(err);
}
};
From the above code I'm trying to navigate to dashboard on successful login.
The auth handler function is being called once the submit button is clicked.
The login details are successfully got in authhandler function, but once I use history to navigate I get the following error
"Uncaught (in promise) Error: Invalid hook call. Hooks can only be called inside of the body of a function component"

Error text is pretty clear. You can not call useHistory, or any other hook, outside of functional component. Also, hooks must be called unconditionally, on top of component. Try to call useHistory inside your actual component and pass history as a parameter to authHandler.

The problem is that authHandler is an async function and using a hook inside a "normal" function don't work. It breaks the rule of hooks.
What you need to do is separate authHandler and history.push('/dashboard').
What you can do is return the async request and use .then to call history.push.
const authHandler = async (email, password) => {
const authService = new AuthService();
// returning the request
return await authService
.login({
email,
password
})
};
And inside your component you use the useHistory hook and call authHandler on some action.
const MyComponent = () => {
const history = useHistory()
const onClick = (email, password) => {
authHandler(email, password)
.then(() => history.push('/dashboard'))
}
return (...)
}

Related

What is the correct way to use UseNavigate inside axios interceptor [duplicate]

This question already has answers here:
React router v6 how to use `navigate` redirection in axios interceptor
(6 answers)
Closed 4 months ago.
I have created an axios interceptor for my service, scenario is if specific api calls fails. It will redirect to error component. But it gives me error Error: Invalid hook call. Hooks can only be called inside of the body of a function component.
Here is my code for agent.ts:
axios.defaults.baseURL = "https://localhost:5001/api/";
const responseBody = (response: AxiosResponse) => response.data;
axios.interceptors.response.use(
(response) => {
return response;
},
(error: AxiosError) => {
const { data, status }: { data: any; status: any } = error.response!;
const navigate = useNavigate();
switch (status) {
case 500:
//toast.error(data.title);
navigate("server-error");
break;
default:
break;
}
return Promise.reject(error.response);
}
);
const request = {
get: (url: string) => axios.get(url).then(responseBody),
};
const Catalog = {
list: () => request.get("products"),
};
const agent = {
Catalog,
};
And in my component, here is the code for calling the request:
useEffect(() => {
agent.Catalog.list().then((products) => setProducts(products));
}, []);
As mentioned in the error, hooks can only be used inside a function component. By using useNavigate inside a function that isn't a function component would simply yield an erroneous result.
You can create a hook that intercepts requests and use useNavigate within the resulting interception.
The code below demonstrates how to consume an axios interceptor in a react component.
Working codesandbox example: Please note that you need to open the web app in a new window instead of the codesandox iframe, since I'm using msw to simulate a 500 status code request. You can visit the non-iframe version in this link.
Navigate to the "Page with Error" link, it should send a request to a URL that responds a 500 status code. The axios interceptor should direct the page back to the / route.
// AxiosNavigation.js
import { useRef, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import axios from 'axios';
export function useAxiosNavigation() {
// Use useRef to prevent a re-render in the useEffect.
// A ref, cannot be used as a useEffect dependency, hence,
// your linters shouldn't complain about missing dependencies.
const navRef = useRef(useNavigate());
useEffect(() => {
const intercetpor = axios.interceptors.response.use(
(response) => response,
(error) => {
switch (error?.response?.status) {
case 500:
navRef.current('/');
break;
default:
}
return Promise.reject(error);
}
);
return () => {
axios.interceptors.response.eject(intercetpor);
};
}, []);
}
export default function AxiosNavigation() {
useAxiosNavigation();
return <></>;
}
To use the component above, we need to add it inside a router, the case below uses MemoryRouter but you can freely change it to BrowserRouter if you want to.
// App.js
import { MemoryRouter, Routes, Route, Link } from 'react-router-dom';
import AxiosNavigation from './AxiosNavigation';
import PageWithError from './Page-With-Error';
export default function App() {
return (
<main>
<MemoryRouter>
<AxiosNavigation />
<header>
<nav>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
<Link to="/page-with-error">Page with error</Link>
</nav>
</header>
<article>
<Routes>
<Route path="/" element={<>Home Page</>} />
<Route path="/about" element={<>About Page</>} />
<Route path="/page-with-error" element={<PageWithError />} />
</Routes>
</article>
</MemoryRouter>
</main>
);
}
The PageWithError component also has to be defined, it simply sends a request to an API that returns 500 status code. As described in the axios interceptor hook we implemented above, it simply goes to the root page, /.
// PageWithError.js
import axios from 'axios';
import { useState, useEffect } from 'react';
export default function PageWithError() {
const [loading, setLoading] = useState(false);
useEffect(() => {
setLoading(true);
axios
.get('/api-500-error')
.catch((error) => {
console.log('caught', error.response);
})
.finally(() => {
setLoading(false);
});
}, []);
return <>Page With Error: {loading && 'going to home page...'}</>;
}

react-router-dom useHistory() not working

The useHistory() hook is not working in my project. I have it in different components but none of them work.
I am using "react-router-dom": "^5.2.0",
import {useHistory} from 'react-router-dom'
const history = useHistory()
const logout = () => {
toggleMenu()
setUser(null)
dispatch({type: 'LOGOUT'})
history.push('/')
}
And also in action its not working
export const signin = (formdata, history, setError) => async (dispatch) => {
try {
const {data} = await API.signIn(formdata)
if(data.error){
setError(data.message)
}else{
dispatch({type: SIGNIN, data})
history.push('/dashboard')
}
} catch (error) {
setError(error)
console.log(error)
}
}
Here is my router.js file
const Router = () => {
const user = JSON.parse(localStorage.getItem('profile'))
return (
<BrowserRouter>
<>
{user && <Navigation/>}
<Switch>
<Route path='/page-not-found' component={Auth}/>
<Route path='/' exact component={()=>((user && user.result) ? <Redirect to="/dasboard"/> : <Auth/>)} />
<Route path='/dashboard' component={() => (user ? <Dashboard/> : <Redirect to='/'/>)} />
<Route path='/books-purchase' component={() => (user ? <BooksPage/> : <Redirect to='/' />)} />
</Switch>
</>
</BrowserRouter>
)
}
For the version of react router dom less than 6.^
You can use useHistory() hook like your code is showing
For the latest version of react router dom greater than 6.^
You can use useNavigate() hook like this. You can also use without props
import { useNavigate } from 'react-router-dom';
class Login extends Component {
....
let navigate = useNavigate();
navigate('/');
.....
Instead of useHistory, try useNavigate:
import { useNavigate } from 'react-router-dom';
After some investigation, I found that there is a bug in react-router-dom version ^5.2.0. See this and this . I would suggest you to downgrade react-router-dom version to 4.10.1
In my point of view, It doesn't need to downgrade react-router-dom, just use "useNavigate" hook instead of "useHistory" hook. For example if you also want to send some data to your dashboard page, you can use it like so:
import { useNavigate } from 'react-router-dom'
const FunctionalComponent = () => {
const navigate = useNavigate();
const TestHandler = (someData) => {
navigate("/dashboard", {state: {someData: someData}});
//and if you don't want to send any data use like so:
//navigate("/dashboard");
}
}
Finally, if you want to use that sent data for example in your dashboard page, you can do it like so:
import { useLocation } from 'react-router-dom'
export const Dashboard = () => {
const location = useLocation();
useEffect(() => {
if (location.state.someData) {
// "someData" is available here.
}
});
You can refer to this migration guide: https://reactrouter.com/docs/en/v6/upgrading/v5
i have same error, I forgot add type to button that working function. when i give type,problem was solved
Sometimes, if we call history in dialog or modal it shows error, pass the method to components
const methodTopass= () => {
history.push("/route");
};
<componet methodTopass/>
left "react-router-dom": "^5.2.0", add "history": "4.10.1", in to package.json
then use import { createBrowserHistory } from "history";
and
let hist = createBrowserHistory();
hist.push("/domoy");

How can I prevent a React state update on an unmounted component in my integration testing?

I'm using the testing-library to write my tests. I'm writing integration tests that load the component and then trying to walk through the UI in the test to mimic what a user might do and then testing the results of these steps. In my test output, I get the following warning when both tests run but do not get the following warning when only one test is run. All tests that run pass successfully.
console.error node_modules/react-dom/cjs/react-dom.development.js:88
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
in Unknown (at Login.integration.test.js:12)
The following is my integration test written in jest. if I comment out either one of the tests, the warning goes away but if they both run then I get the warning.
import React from 'react';
import { render, screen, waitForElementToBeRemoved, waitFor } from '#testing-library/react';
import userEvent from "#testing-library/user-event";
import { login } from '../../../common/Constants';
import "#testing-library/jest-dom/extend-expect";
import { MemoryRouter } from 'react-router-dom';
import App from '../../root/App';
import { AuthProvider } from '../../../middleware/Auth/Auth';
function renderApp() {
render(
<AuthProvider>
<MemoryRouter>
<App />
</MemoryRouter>
</AuthProvider>
);
//Click the Login Menu Item
const loginMenuItem = screen.getByRole('link', { name: /Login/i });
userEvent.click(loginMenuItem);
//It does not display a login failure alert
const loginFailedAlert = screen.queryByRole('alert', { text: /Login Failed./i });
expect(loginFailedAlert).not.toBeInTheDocument();
const emailInput = screen.getByPlaceholderText(login.EMAIL);
const passwordInput = screen.getByPlaceholderText(login.PASSWORD);
const buttonInput = screen.getByRole('button', { text: /Submit/i });
expect(emailInput).toBeInTheDocument();
expect(passwordInput).toBeInTheDocument();
expect(buttonInput).toBeInTheDocument();
return { emailInput, passwordInput, buttonInput }
}
describe('<Login /> Integration tests:', () => {
test('Successful Login', async () => {
const { emailInput, passwordInput, buttonInput } = renderApp();
Storage.prototype.getItem = jest.fn(() => {
return JSON.stringify({ email: 'asdf#asdf.com', password: 'asdf' });
});
// fill out and submit form with valid credentials
userEvent.type(emailInput, 'asdf#asdf.com');
userEvent.type(passwordInput, 'asdf');
userEvent.click(buttonInput);
//It does not display a login failure alert
const noLoginFailedAlert = screen.queryByRole('alert', { text: /Login Failed./i });
expect(noLoginFailedAlert).not.toBeInTheDocument();
// It hides form elements
await waitForElementToBeRemoved(() => screen.getByPlaceholderText(login.EMAIL));
expect(emailInput).not.toBeInTheDocument();
expect(passwordInput).not.toBeInTheDocument();
expect(buttonInput).not.toBeInTheDocument();
});
test('Failed Login - invalid password', async () => {
const { emailInput, passwordInput, buttonInput } = renderApp();
Storage.prototype.getItem = jest.fn(() => {
return JSON.stringify({ email: 'brad#asdf.com', password: 'asdf' });
});
// fill out and submit form with invalid credentials
userEvent.type(emailInput, 'brad#asdf.com');
userEvent.type(passwordInput, 'invalidpw');
userEvent.click(buttonInput);
//It displays a login failure alert
await waitFor(() => expect(screen.getByRole('alert', { text: /Login Failed./i })).toBeInTheDocument())
// It still displays login form elements
expect(emailInput).toBeInTheDocument();
expect(passwordInput).toBeInTheDocument();
expect(buttonInput).toBeInTheDocument();
});
});
The following is the component:
import React, { useContext } from 'react';
import { Route, Switch, withRouter } from 'react-router-dom';
import Layout from '../../hoc/Layout/Layout';
import { paths } from '../../common/Constants';
import LandingPage from '../pages/landingPage/LandingPage';
import Dashboard from '../pages/dashboard/Dashboard';
import AddJob from '../pages/addJob/AddJob';
import Register from '../pages/register/Register';
import Login from '../pages/login/Login';
import NotFound from '../pages/notFound/NotFound';
import PrivateRoute from '../../middleware/Auth/PrivateRoute';
import { AuthContext } from '../../middleware/Auth/Auth';
function App() {
let authenticatedRoutes = (
<Switch>
<PrivateRoute path={'/dashboard'} exact component={Dashboard} />
<PrivateRoute path={'/add'} exact component={AddJob} />
<PrivateRoute path={'/'} exact component={Dashboard} />
<Route render={(props) => (<NotFound {...props} />)} />
</Switch>
)
let publicRoutes = (
<Switch>
<Route path='/' exact component={LandingPage} />
<Route path={paths.LOGIN} exact component={Login} />
<Route path={paths.REGISTER} exact component={Register} />
<Route render={(props) => (<NotFound {...props} />)} />
</Switch>
)
const { currentUser } = useContext(AuthContext);
let routes = currentUser ? authenticatedRoutes : publicRoutes;
return (
<Layout>{routes}</Layout>
);
}
export default withRouter(App);
The following is the AuthProvider component that wraps in the renderApp() function. It takes advantadge of the React useContext hook to manage the state of a users authentication for the app:
import React, { useEffect, useState } from 'react'
import { AccountHandler } from '../Account/AccountHandler';
export const AuthContext = React.createContext();
export const AuthProvider = React.memo(({ children }) => {
const [currentUser, setCurrentUser] = useState(null);
const [pending, setPending] = useState(true);
useEffect(() => {
if (pending) {
AccountHandler.getInstance().registerAuthStateChangeObserver((user) => {
setCurrentUser(user);
setPending(false);
})
};
})
if (pending) {
return <>Loading ... </>
}
return (
<AuthContext.Provider value={{ currentUser }}>
{children}
</AuthContext.Provider>
)
});
It seems as though the first test mounts the component under test but that the second test is somehow trying to reference the first mounted component rather than the newly mounted component but I can't seem to figure out exactly what's happening here to correct these warnings. Any help would be greatly appreciated!
the AccountHandler isn't a singleton() the getInstance method name needs to be refactored to reflect this. So a new instance of AccountHandler is being created every time this is called. But, the register function adds an observer to an array that is iterated and each observer is called in that array when the authentication state changes. I wasn't clearing when new observers were added and thus the tests were calling both the old and unmounted observers as well as the new ones. By simply clearing that array, the issue was resolved. Here's the corrected code that has fixed the issue:
private observers: Array<any> = [];
/**
*
* #param observer a function to call when the user authentication state changes
* the value passed to this observer will either be the email address for the
* authenticated user or null for an unauthenticated user.
*/
public registerAuthStateChangeObserver(observer: any): void {
/**
* NOTE:
* * The observers array needs to be cleared so as to avoid the
* * situation where a reference to setState on an unmounted
* * React component is called. By clearing the observer we
* * ensure that all previous observers are garbage collected
* * and only new observers are used. This prevents memory
* * leaks in the tests.
*/
this.observers = [];
this.observers.push(observer);
this.initializeBackend();
}
It looks like your AccountHandler is a singleton and you subscribe to changes to it.
This means that after you unmount the first component and mount the second instance, the first one is still registered there, and any updates to the AccountHandler will trigger the handler which calls setCurrentUser and setPending of the first component too.
You need to unsubscribe when the component is unmounted.
something like this
const handleUserChange = useCallback((user) => {
setCurrentUser(user);
setPending(false);
}, []);
useEffect(() => {
if (pending) {
AccountHandler.getInstance().registerAuthStateChangeObserver(handleUserChange)
};
return () => {
// here you need to unsubscribe
AccountHandler.getInstance().unregisterAuthStateChangeObserver(handleUserChange);
}
}, [])

Simple logout component throws "Cannot update a component from inside the function body of a different component"

This little Logout.jsx component logs-out the user...
import React from 'react';
import { Redirect } from 'react-router';
import { useDispatch } from 'react-redux';
import { userLogout } from '../redux/actions/authActions';
const Logout = ({ to = '/loginForm' }) => {
const dispatch = useDispatch();
dispatch(userLogout());
return <Redirect to={to} />;
};
export default Logout;
and is used in path /logout thus:
<Switch>
...
<Route exact path="/logout" component={Logout} />
In the console it gives the dreaded (and apparently serious) message:
Cannot update a component from inside the function body of a different
component
Can someone spot why, and how to fix it?
Using react 16.13.0
I think it's just a logical mistake causing this error to pop up from another component (than Logout), try logging out once:
const Logout = ({ to = '/loginForm' }) => {
const dispatch = useDispatch();
useEffect(() => {
dispatch(userLogout());
}, [dispatch]);
return <Redirect to={to} />;
};
You don't want to dispatch (or logout) on every component render

How to maintain the authenticated user state on page refresh with react

I have some protected routers but I need to maintain the state on page refresh.
import React from 'react'
import { Route, Redirect, Switch } from 'react-router-dom'
const auth = {
isAuthenticated: true // this would be an http call to get user data
}
const PrivateRoute = ({ component: Component, ...rest }) => (
<Route {...rest} render={(props) => (
auth.isAuthenticated === true
? <Component {...props} />
: <Redirect to="/login" />
)} />
)
const Main = () => {
return (
<Switch>
<Route path="/login" exact strict component={Login}/>
<Route path="/logout" exact strict component={Logout}/>
<PrivateRoute path="/profile" exact strict component={Profile}/>
</Switch>
)
}
export default Main
where should I make the service call? in the Main app? in a context?
update: I added a check in Main that makes a call to the api sending the token that I have stored. that call could return 200 with data or 401
const auth = {
isAuthenticated: Api.status() // this is just a fetch to /status endpoint
.then(
status => {
console.log(status);
return true;
},
error => {
console.log(error);
return false;
}
)
}
but when i hit /profile it redirects me immediately to login (because isAuthenticated is false)
my question is based entirely on the scenario where the user refresh the page (F5) other scenarios are working fine. sorry If I´m not clear enough happy to clarify anything thanks again!
the best way in my opinion is the local storage save the token in there dispatch an action in componentDidMount() in the root component of you app and store the token in readucer state
You can implement an auth-context like shown here: https://usehooks.com/useAuth/
And in addition store user in localStorage (or storage of your preference) and when the auth-context mounts read from localStorage and set the initial value like so:
const [user, setUser] = useState(JSON.parse(localStorage.getItem('user')));
This way the protected routes will automatically redirect (or whatever they're supposed to do in an authenticated state) when reloading the page.
In this case I'd make an AuthContext component. By using the context provider, you can access the user logged in state everywhere in your app.
import { createContext } from 'react';
const AuthContext = createContext({
token: null,
userId: null,
login: () => {},
logout: () => {}
});
export default AuthContext;
And provide this at the highest level in your app.
import React, { useState } from 'react';
import { Route, Switch } from 'react-router-dom';
import AuthContext from './context/AuthContext';
function App() {
const [token, setToken] = useState(null);
const [userId, setUserId] = useState(null);
const login = (token, userId) => {
setToken(token);
setUserId(userId);
};
const logout = () => {
setToken(null);
setUserId(null);
};
return (
<Switch>
<Route path="/login" exact strict component={Login}/>
<Route path="/logout" exact strict component={Logout}/>
<PrivateRoute path="/profile" exact strict component={Profile}/>
</Switch>
)
}
export default App;
And in a login/logout form you make the call to a database or localstorage.
import React, { useContext } from 'react';
import AuthContext from '../context/AuthContext';
function Auth() {
const { login } = useContext(AuthContext);
const submitHandler = () => {
// Make the call here
};
return (
<Form onSubmit={submitHandler}>
// Put your login form here
</Form>
);
}
export default Auth;

Resources