'How to prevent protected route from automatically redirecting on redux state change
I have created two route components, one for authenticated users and the other for guest. each of the routes checks on the redux state for the authenticated property to determine if a user is logged in or not in order to make the appropriate redirection. Now, the issues is that the checking and redirection should only happen once when a user changes the route but instead each of the route components re-renders when the authenticated property changes on the redux state. This is affecting my login flow because the guest route components automatically handles the redirection instead of the login component.
Below is code:
App.js component:
import './App.css'
import { lazy, useEffect } from 'react'
import { Route, BrowserRouter as Router, Routes } from 'react-router-dom'
import Layout from './components/Layout'
import AuthRoute from './components/AuthRoute'
import Error from './components/ErrorPage'
import configData from './config/configData.json'
import axios from 'axios'
// import { useEffect } from 'react'
// Import Css
import './assets/css/materialdesignicons.min.css'
import './Apps.scss'
import './assets/css/colors/default.css'
import SetupBusiness from './components/Business/SetupBusiness'
import Preview from './components/Business/Preview'
import About from './components/Business/About'
import Media from './components/Business/Media'
import { logoutAction } from './state/action-creators/AuthAction'
import { connect } from 'react-redux'
import PublicRoute from './components/PublicRoute'
const Register = lazy(() => import('./controller/Register'))
const Login = lazy(() => import('./controller/Login'))
function App(props) {
useEffect(() => {
async function checkUserAuth() {
const token = localStorage.getItem('token')
if (token) {
axios
.get(`${configData.SERVER_URL}/user/me`, {
headers: {
Authorization: `Bearer ${token}`,
},
})
.then(() => {})
.catch((error) => {
console.log('nbvhg')
props.logout()
})
}
}
checkUserAuth()
setInterval(checkUserAuth, 15000)
}, [props])
return (
<div className="App">
<Router>
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<h1>Homepage</h1>} />
<Route path="/" element={<PublicRoute />}>
<Route path="/register" element={<Register />} />
<Route path="/login" element={<Login />} />
</Route>
<Route path="/business/setup" element={<AuthRoute />}>
<Route element={<SetupBusiness />}>
<Route path="about" element={<About />} />
<Route path="media" element={<Media />} />
<Route path="preview" element={<Preview />} />
</Route>
</Route>
</Route>
<Route path="*" element={<Error />} />
</Routes>
</Router>
</div>
)
}
const mapDispatchToProps = (dispatch) => {
return {
logout: () => {
return dispatch(logoutAction())
},
}
}
export default connect(null, mapDispatchToProps)(App)
PublicRoute component:
import { connect } from 'react-redux'
import { Navigate, Outlet } from 'react-router-dom'
import React from 'react'
class PublicRoute extends React.Component {
render() {
const { authenticated } = this.props
if (authenticated) {
return <Navigate to="/" />
}
return <Outlet />
}
}
const mapStateToProps = (state) => ({
authenticated: state.auth.authenticated,
})
export default connect(mapStateToProps, null)(PublicRoute)
Login component:
// React Basic and Bootstrap
import React from 'react'
import { connect } from 'react-redux'
import Cookies from 'js-cookie'
import { loginAction } from '../state/action-creators/AuthAction'
import View from '../components/Login'
class Login extends React.Component {
constructor(props) {
super(props)
this.state = {
values: {
email_address: '',
password: '',
},
errors: {},
isSubmitting: false,
redirect: null,
}
this.handleChange = this.handleChange.bind(this)
this.handleSubmit = this.handleSubmit.bind(this)
}
handleChange(event) {
let values = Object.assign({}, this.state.values)
let target = event.target
let name = target.name
let value = target.value
if (typeof values[name] !== 'undefined') {
if (name === 'checked') {
value = target.checked
}
values[name] = value
}
this.setState({ values })
}
validation() {
const errors = {}
let values = this.state.values
//email validation
if (!values.email_address.trim()) {
errors.email_address = 'Email is required'
} else if (!/\S+@\S+\.\S+/.test(values.email_address)) {
errors.email_address = 'Email is invalid'
}
//password validation
if (!values.password.trim()) {
errors.password = 'password is required'
} else if (values.password < 8) {
errors.password = 'PassWord need to be 8 characters or more'
}
return errors
}
async handleSubmit(event) {
if (event) {
event.preventDefault()
}
this.setState(
{ isSubmitting: true, errors: this.validation() },
async () => {
if (Object.keys(this.state.errors).length === 0) {
try {
const response = await this.props.login(this.state.values)
if (response) {
const redirect = Cookies.get('redirect')
if (redirect) {
this.setState({ redirect })
Cookies.remove('redirect')
} else {
this.setState({ redirect: '/' })
}
}
} catch (error) {
console.log(error)
} finally {
// this.setState({ isSubmitting: false })
}
} else {
this.setState({ isSubmitting: false })
}
}
)
}
render() {
return (
<View
onChange={this.handleChange}
values={this.state.values}
errors={this.state.errors}
errorMessage={this.props.errorMessage}
onSubmit={this.handleSubmit}
isSubmitting={this.state.isSubmitting}
redirect={this.state.redirect}
/>
)
}
}
const mapDispatchToProps = (dispatch) => {
return {
login: (values) => {
return dispatch(loginAction(values))
},
}
}
const mapStateToProps = (state) => {
return {
errorMessage: state.auth.errorMessage,
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Login)
AuthRoute component
import { logoutAction } from '../state/action-creators/AuthAction'
import { connect } from 'react-redux'
import { Navigate, Outlet } from 'react-router-dom'
import React from 'react'
import Cookies from 'js-cookie'
class AuthRoute extends React.Component {
render() {
const { authenticated, logout } = this.props
if (!authenticated) {
Cookies.set('redirect', window.location.pathname, { path: '/' })
logout()
return <Navigate to="/login" />
}
return <Outlet />
}
}
const mapDispatchToProps = (dispatch) => {
return {
logout: () => {
return dispatch(logoutAction())
},
}
}
const mapStateToProps = (state) => ({
authenticated: state.auth.authenticated,
})
export default connect(mapStateToProps, mapDispatchToProps)(AuthRoute)
Login action
import { formatError, login } from './AuthService'
export const CLEAR_ERROR_MESSAGE = '[register action] clear error message'
export const LOGIN_CONFIRMED_ACTION = '[login action] confirmed login'
export const LOGIN_FAILED_ACTION = '[login action] failed login'
export const LOGOUT_ACTION = '[logout action] logout action'
function clearErrorAction() {
return {
type: CLEAR_ERROR_MESSAGE,
}
}
function confirmedLoginAction(payload) {
return {
type: LOGIN_CONFIRMED_ACTION,
payload,
}
}
function failedLoginAction(message) {
return {
type: LOGIN_FAILED_ACTION,
payload: message,
}
}
export function loginAction({ email_address, password }) {
return (dispatch) => {
return login({
email_address,
password,
})
.then((response) => {
const token = response.data.access_token
localStorage.setItem('token', token)
dispatch(confirmedLoginAction(response))
return response
})
.catch((error) => {
if (!error.response) {
dispatch(failedLoginAction('Server error'))
} else {
const errorMessage = formatError(error.response.data)
dispatch(failedLoginAction(errorMessage))
}
throw error
})
}
}
function confirmedLogoutAction() {
return {
type: LOGOUT_ACTION,
}
}
export function logoutAction() {
return (dispatch) => {
localStorage.removeItem('token')
dispatch(confirmedLogoutAction())
}
}
AuthReducer
import {
LOGIN_CONFIRMED_ACTION,
LOGIN_FAILED_ACTION,
CLEAR_ERROR_MESSAGE,
LOGOUT_ACTION,
} from '../action-creators/AuthAction'
const initialState = {
user: {
first_name: '',
last_name: '',
user_name: '',
email_address: '',
password: '',
id: '',
is_archived: '',
projects: '',
date_created: '',
},
access_token: '',
errorMessage: '',
authenticated: !!localStorage.getItem('token'),
}
export function AuthReducer(state = initialState, action) {
if (action.type === CLEAR_ERROR_MESSAGE) {
return {
...state,
errorMessage: '',
}
}
if (action.type === LOGIN_CONFIRMED_ACTION) {
return {
...state,
authenticated: true,
user: action.payload,
}
}
if (action.type === LOGIN_FAILED_ACTION) {
return {
...state,
errorMessage: action.payload,
}
}
if (action.type === LOGOUT_ACTION) {
return {
...state,
authenticated: false,
user: {
first_name: '',
last_name: '',
user_name: '',
email_address: '',
password: '',
id: '',
is_archived: '',
projects: '',
date_created: '',
},
}
}
return state
}
I am getting error "react_devtools_backend.js:3973 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 the componentWillUnmount method." from react because I am trying to setState after login but the the guest route component have already redirected out from the Login component before setting the state hence the error.
Thank you in advance.
Solution 1:[1]
It seems the issue is that the loginAction action creator dispatches a login success action and the redux state is updated, which triggers a rerender. This rerender and updated authenticated state is used by the PublicRoute component and the user is bounced to the home route "/" before the redirect state update occurs in handleSubmit of the Login component.
As far as I can tell it appears you are using the redirect state in Login to pass a redirect prop to the View component to, I assume, render a Navigate component to redirect back to the referrer route.
This extra step/state update/rerender is completely extraneous and unnecessary. It is far more common to issue an imperative navigate from the login handler upon successful authentication. For this the navigate function is used to redirect back. Since the Login component is a class component it can't use the useNavigate hook. The options here are to either convert to function component or create a custom withNavigate HOC.
Example:
import { useNavigate } from 'react-router-dom';
const withNavigate = Component => props => {
const navigate = useNavigate();
return <Component {...props} navigate={navigate} />;
};
export default withNavigate;
Decorate the Login component with the withNavigate HOC and access the navigate function from props and issue the imperative redirect.
import React from 'react';
import { compose } from 'redux';
import { connect } from 'react-redux';
import Cookies from 'js-cookie';
import { loginAction } from '../state/action-creators/AuthAction';
import View from '../components/Login';
import withNavigate from '../path/to/withNavigate';
class Login extends React.Component {
constructor(props) {
super(props)
this.state = {
values: {
email_address: '',
password: '',
},
errors: {},
isSubmitting: false,
};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
...
async handleSubmit(event) {
if (event) {
event.preventDefault();
}
this.setState(
{ isSubmitting: true, errors: this.validation() },
async () => {
if (!Object.keys(this.state.errors).length) {
try {
const response = await this.props.login(this.state.values);
if (response) {
const redirect = Cookies.get('redirect');
this.props.navigate(redirect || "/", { replace: true });
}
} catch (error) {
console.log(error);
// didn't authenticate, clear submitting
this.setState({ isSubmitting: false });
}
} else {
this.setState({ isSubmitting: false });
}
}
);
}
render() {
return (
<View
onChange={this.handleChange}
values={this.state.values}
errors={this.state.errors}
errorMessage={this.props.errorMessage}
onSubmit={this.handleSubmit}
isSubmitting={this.state.isSubmitting}
/>
)
}
}
const mapDispatchToProps = {
login: loginAction,
};
const mapStateToProps = (state) => ({
errorMessage: state.auth.errorMessage,
});
export default compose(
connect(mapStateToProps, mapDispatchToProps),
withNavigate,
)(Login);
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | Drew Reese |
