React JS认证后无法跳转至仪表盘的问题排查
React + Redux Toolkit 登录后无法跳转仪表盘问题排查与修复
问题概述
使用React JS结合Redux/Toolkit开发用户认证功能,通过Formik创建登录表单,但提交表单后无法跳转至仪表盘,无法定位问题所在。
代码模块展示
1. 认证服务 (src/services/AuthService.js)
export async function apiSignIn (data) { return ApiService.fetchData({ url: '/v1/auth/login', method: 'post', data }) }
2. Redux切片
sessionSlice (src/store/auth/sessionSlice.js)
import { createSlice } from '@reduxjs/toolkit' export const sessionSlice = createSlice({ name: 'auth/session', initialState: { token: '', signedIn: false, }, reducers: { onSignInSuccess: (state, action) => { state.signedIn = true state.token = action.payload }, onSignOutSuccess: (state) => { state.signedIn = false state.token = '' }, setToken: (state, action) => { state.token = action.payload } }, }) export const { onSignInSuccess, onSignOutSuccess, setToken } = sessionSlice.actions export default sessionSlice.reducer
userSlice (src/store/auth/userSlice.js)
import { createSlice } from '@reduxjs/toolkit' export const initialState = { avatar: '', userName: '', email: '', authority: [] } export const userSlice = createSlice({ name: 'auth/user', initialState, reducers: { setUser: (_, action) => action.payload, userLoggedOut: () => initialState, }, }) export const { setUser } = userSlice.actions export default userSlice.reducer
3. useAuth钩子 (utils/hooks/useAuth.js)
import { useSelector, useDispatch } from 'react-redux' import { setUser, initialState } from 'store/auth/userSlice' import { apiSignIn, apiSignOut, apiSignUp } from 'services/AuthService' import { onSignInSuccess, onSignOutSuccess } from 'store/auth/sessionSlice' import { REDIRECT_URL_KEY } from 'constants/app.constant' import { useNavigate } from 'react-router-dom' import useQuery from './useQuery' function useAuth() { const dispatch = useDispatch() const navigate = useNavigate() const query = useQuery() const { token, signedIn } = useSelector((state) => state.auth.session) const signIn = async (values) => { try { const resp = await apiSignIn(values) if (resp.data) { const { token } = resp.data dispatch(onSignInSuccess(token)) if(resp.data) { dispatch(setUser(resp.data || { avatar: '', userName: resp.data.userName, authority: [resp.data.role], email: resp.data.email })) } const redirectUrl = query.get(REDIRECT_URL_KEY) navigate(redirectUrl ? redirectUrl : '/dashboard') return { status: 'success', message: '' } } } catch (errors) { return { status: 'failed', message: errors?.response?.data?.message || errors.toString() } } } const signUp = async (values) => { try { const resp = await apiSignUp(values) if (resp.data) { const { token } = resp.data dispatch(onSignInSuccess(token)) if(resp.data.user) { dispatch(setUser(resp.data.user || { avatar: '', userName: 'Anonymous', authority: ['USER'], email: '' })) } const redirectUrl = query.get(REDIRECT_URL_KEY) navigate(redirectUrl ? redirectUrl : '/dashboard') return { status: 'success', message: '' } } } catch (errors) { return { status: 'failed', message: errors?.response?.data?.message || errors.toString() } } } const handleSignOut = () => { dispatch(onSignOutSuccess()) dispatch(setUser(initialState)) navigate('/dashboard') } const signOut = async () => { await apiSignOut() handleSignOut() } return { authenticated: token && signedIn, signIn, signUp, signOut } } export default useAuth
4. 登录表单 (components/SignInForm.js)
import React from 'react' import { Input, Button, Checkbox, FormItem, FormContainer, Alert } from 'components/ui' import { PasswordInput, ActionLink } from 'components/shared' import useTimeOutMessage from 'utils/hooks/useTimeOutMessage' import { Field, Form, Formik } from 'formik' import * as Yup from 'yup' import useAuth from 'utils/hooks/useAuth' const validationSchema = Yup.object().shape({ userName: Yup.string().required('Please enter your username'), password: Yup.string().required('Please enter your password'), rememberMe: Yup.bool() }) const SignInForm = props => { const { disableSubmit = false, className, } = props const [message, setMessage] = useTimeOutMessage() const { signIn } = useAuth() const onSignIn = async (values, setSubmitting) => { const { userName, password } = values setSubmitting(true) const result = await signIn({ userName, password }) if (result.status === 'failed') { setMessage(result.message) } setSubmitting(false) } return ( <div className={className}> {message && <Alert className="mb-4" type="danger" showIcon>{message}</Alert>} <Formik initialValues={{ userName: '', password: '', rememberMe: true }} validationSchema={validationSchema} onSubmit={(values, { setSubmitting }) => { if(!disableSubmit) { onSignIn(values, setSubmitting) } else { setSubmitting(false) } }} > {({touched, errors, isSubmitting}) => ( <Form> <FormContainer> <FormItem label="User Name" invalid={errors.userName && touched.userName} errorMessage={errors.userName} > <Field type="text" autoComplete="off" name="userName" placeholder="User Name" component={Input} /> </FormItem> <FormItem label="Password" invalid={errors.password && touched.password} errorMessage={errors.password} > <Field autoComplete="off" name="password" placeholder="Password" component={PasswordInput} /> </FormItem> <div className="flex justify-between mb-6"> <Field className="mb-0" name="rememberMe" component={Checkbox} children="Remember Me" /> </div> <Button block loading={isSubmitting} variant="solid" type="submit"> { isSubmitting ? 'Signing in...' : 'Sign In' } </Button> </FormContainer> </Form> )} </Formik> </div> ) } export default SignInForm
可能的问题点与修复方案
1. API响应判断逻辑冗余,可能导致跳转未执行
在signIn函数中,if(resp.data)判断重复,且如果API返回的resp.data结构不符合预期(比如token不存在),会跳过后续逻辑。
修复:
const signIn = async (values) => { try { const resp = await apiSignIn(values) // 确保响应存在且包含token if (resp?.data?.token) { const { token, userName, role, email } = resp.data dispatch(onSignInSuccess(token)) // 明确构造用户对象,避免依赖resp.data的完整结构 dispatch(setUser({ avatar: '', userName: userName || '', authority: role ? [role] : [], email: email || '' })) const redirectUrl = query.get(REDIRECT_URL_KEY) // 使用replace避免回退到登录页 navigate(redirectUrl ? redirectUrl : '/dashboard', { replace: true }) return { status: 'success', message: '' } } else { return { status: 'failed', message: '登录失败,未获取到有效凭证' } } } catch (errors) { return { status: 'failed', message: errors?.response?.data?.message || errors.toString() } } }
2. Redux状态更新是异步的,跳转可能早于状态生效
虽然Redux Toolkit的dispatch是同步触发,但组件状态更新是异步的。如果仪表盘路由有基于authenticated状态的守卫,可能会因为状态未更新导致跳转被拦截。
修复:
在useAuth中监听authenticated状态,确保状态变为true后再跳转:
import { useEffect } from 'react' function useAuth() { const dispatch = useDispatch() const navigate = useNavigate() const query = useQuery() const { token, signedIn } = useSelector((state) => state.auth.session) const authenticated = token && signedIn // 新增:监听认证状态变化,自动跳转 useEffect(() => { if (authenticated) { const redirectUrl = query.get(REDIRECT_URL_KEY) navigate(redirectUrl ? redirectUrl : '/dashboard', { replace: true }) } }, [authenticated, navigate, query]) const signIn = async (values) => { try { const resp = await apiSignIn(values) if (resp?.data?.token) { const { token, userName, role, email } = resp.data dispatch(onSignInSuccess(token)) dispatch(setUser({ avatar: '', userName: userName || '', authority: role ? [role] : [], email: email || '' })) return { status: 'success', message: '' } } else { return { status: 'failed', message: '登录失败,未获取到有效凭证' } } } catch (errors) { return { status: 'failed', message: errors?.response?.data?.message || errors.toString() } } } // 其余代码不变... }
3. Formik提交逻辑未处理signIn的异常情况
当前onSignIn函数中,如果signIn抛出异常(比如API请求失败但未被catch),会导致setSubmitting(false)不执行,表单一直处于提交状态。
修复:
const onSignIn = async (values, setSubmitting) => { const { userName, password } = values setSubmitting(true) try { const result = await signIn({ userName, password }) if (result.status === 'failed') { setMessage(result.message) } } catch (err) { setMessage('登录请求异常,请稍后重试') } finally { setSubmitting(false) } }
4. 检查路由配置是否包含权限拦截
确保仪表盘路由/dashboard的守卫逻辑正确,不会因为Redux状态未及时更新而拒绝访问。例如,路由守卫中应该允许刚登录的用户访问:
// 示例路由守卫逻辑 const DashboardRoute = () => { const { authenticated } = useAuth() const location = useLocation() if (!authenticated) { return <Navigate to="/login" state={{ from: location }} replace /> } return <Dashboard /> }
调试建议
- 在
signIn函数中添加console.log(resp.data),查看API返回的具体数据结构是否符合预期 - 在Redux DevTools中查看
onSignInSuccess和setUser动作是否正确触发,状态是否更新 - 在
navigate调用前添加console.log('navigating to', redirectUrl || '/dashboard'),确认跳转逻辑是否执行
内容的提问来源于stack exchange,提问作者Anas Boukricha
相关产品推荐
相关产品推荐

