You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.08 17:45:12