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

Redux Toolkit调度后状态未即时更新,需等到下一次渲染

问题:RTK异步登录Action完成后,组件无法立即获取更新后的Redux状态

我在React中用Redux Toolkit(RTK)和Firebase实现登录流程,登录页面中调用dispatch(loginUser(formData))后,立刻通过user.accessToken判断是否登录成功并跳转路由,但发现此时user还是旧状态,要等到组件重新渲染后才会更新。

核心代码展示

登录页面核心逻辑

export default function LoginPage() {
    const isLoading = useAppSelector(state => state.auth.loading)
    const user = useAppSelector(state => state.auth.user)
    const dispatch = useAppDispatch()
    const navigate = useNavigate()
    const [errorMessage, setErrorMessage] = useState("")

    // ...其他状态与输入处理函数

    const onSubmitAction = async (e: React.ChangeEvent<HTMLFormElement>) => {
        e.preventDefault()
        setErrorMessage("")
        try {
            await dispatch(loginUser(formData))
            // 此处的user仍是dispatch执行前的旧值
            if (!user.accessToken) {
                setErrorMessage("Login error: check your email and password and try again")
            }else{
                toast.success('Login Successful', {
                    position: "top-center",
                    autoClose: 5000,
                    hideProgressBar: false,
                    closeOnClick: true,
                    pauseOnHover: true,
                    draggable: true,
                    theme: "dark",
                    transition: Bounce,
                });
                navigate("/user")
            }
        } catch (error) {
            toast.error('An error occured, please try again later!', {
                position: "top-center",
                autoClose: 5000,
                hideProgressBar: false,
                closeOnClick: true,
                pauseOnHover: true,
                draggable: true,
                theme: "dark",
                transition: Bounce,
            });
        }
    }

    // ...页面渲染部分
}

Auth Slice中的登录Action与状态更新

const cookies = new Cookies()

type AuthStateType = {
    loading: boolean,
    user: User,
    error: any,
}

// ...其他类型定义

let initialState: AuthStateType = {
    loading: false,
    user: {
        name: "",
        email: "",
        type: "normal",
        accessToken: "",
        orgName: "",
        isEmailVerified: false,
    },
    error: null
}
const storedToken = cookies.get('token') 
const storedData = cookies.get('userData')
if(storedToken && storedData){
    initialState.user = {
        ...initialState.user,
        accessToken: storedToken,
        ...storedData
    }
}

// 登录异步Action
export const loginUser = createAsyncThunk("auth/login",
    async (payload: LoginRequestType ) =>{
        try {
            await setPersistence(auth, browserLocalPersistence);
            const userCredentials = await signInWithEmailAndPassword(auth, payload.email, payload.password)
            const user = userCredentials.user
            const token = await user.getIdToken()
            const docRef = doc(db, "users", user.uid);
            const userDocSnap = await getDoc(docRef)

            let userData
            let error = null
            if (userDocSnap.exists()) {
                userData = userDocSnap.data()
            } else {
                error = "User does not exist on the db"
            }

            cookies.set("token", token)
            if(userData){
                cookies.set("userData", JSON.stringify(userData))
            }

            return { user, token, userData, error }
        } catch (error) {
            throw error
       }
    }
)

const authSlice = createSlice({
    name: 'auth',
    initialState: initialState,
    reducers:{
        signOut:(state) =>{
            state.user = {
                ...state.user,
                accessToken:"",
                email: "",
                name:"",
                orgName: "",
                type:"normal"
            }
            cookies.remove('token');
            cookies.remove('userData');
        }
    },
    extraReducers: (builder) =>{
        // ...注册相关case
        // 登录状态更新
        builder.addCase(loginUser.pending, (state) => {
            state.loading = true
        })
        builder.addCase(loginUser.fulfilled, (state, { payload }) => {
            state.loading = false
            state.error = null
            state.user = {
                ...state.user,
                accessToken: payload.token,
                email: payload.userData?.email,
                name: payload.userData?.name,
                orgName: payload.userData?.orgName,
                isEmailVerified: payload.userData?.isEmailVerified
            }
        })
        builder.addCase(loginUser.rejected, (state, action) => {
            state.loading = false
            state.error = action.error.message
        })
    }
})

export const authActions = authSlice.actions
export default authSlice.reducer

问题原因

await dispatch(loginUser(formData))确实会等待异步Action执行完成(即loginUser.fulfilled处理完Redux状态更新),但组件中通过useAppSelector获取的user变量是当前渲染周期闭包捕获的旧值。Redux状态更新后需要触发组件重新渲染,才能拿到新的user值,而当前函数执行时组件还未完成重渲染,所以判断逻辑会出错。

解决方案

方案1:直接使用dispatch返回的Promise结果

RTK的createAsyncThunk返回的Action被dispatch后,会返回一个Promise,其resolve值就是fulfilled时的payload。可以直接用这个结果判断登录状态,无需依赖组件的user变量:

const onSubmitAction = async (e: React.ChangeEvent<HTMLFormElement>) => {
    e.preventDefault()
    setErrorMessage("")
    try {
        const result = await dispatch(loginUser(formData))
        // 判断Action是否成功完成
        if (loginUser.fulfilled.match(result)) {
            const { token, userData, error } = result.payload
            if (error) {
                setErrorMessage(error)
            } else if (token && userData) {
                toast.success('Login Successful', {
                    position: "top-center",
                    autoClose: 5000,
                    hideProgressBar: false,
                    closeOnClick: true,
                    pauseOnHover: true,
                    draggable: true,
                    theme: "dark",
                    transition: Bounce,
                });
                navigate("/user")
            } else {
                setErrorMessage("Login error: check your email and password and try again")
            }
        } else {
            setErrorMessage("Login error: check your email and password and try again")
        }
    } catch (error) {
        toast.error('An error occured, please try again later!', {
            position: "top-center",
            autoClose: 5000,
            hideProgressBar: false,
            closeOnClick: true,
            pauseOnHover: true,
            draggable: true,
            theme: "dark",
            transition: Bounce,
        });
    }
}

方案2:用useEffect监听Redux状态变化

监听user.accessToken的变化,当它从空值变为有效token时,执行跳转和成功提示:

export default function LoginPage() {
    const isLoading = useAppSelector(state => state.auth.loading)
    const user = useAppSelector(state => state.auth.user)
    const dispatch = useAppDispatch()
    const navigate = useNavigate()
    const [errorMessage, setErrorMessage] = useState("")

    // 监听accessToken变化,触发登录成功逻辑
    useEffect(() => {
        if (user.accessToken) {
            toast.success('Login Successful', {
                position: "top-center",
                autoClose: 5000,
                hideProgressBar: false,
                closeOnClick: true,
                pauseOnHover: true,
                draggable: true,
                theme: "dark",
                transition: Bounce,
            });
            navigate("/user")
        }
    }, [user.accessToken, navigate])

    const onSubmitAction = async (e: React.ChangeEvent<HTMLFormElement>) => {
        e.preventDefault()
        setErrorMessage("")
        try {
            await dispatch(loginUser(formData))
        } catch (error) {
            toast.error('An error occured, please try again later!', {
                position: "top-center",
                autoClose: 5000,
                hideProgressBar: false,
                closeOnClick: true,
                pauseOnHover: true,
                draggable: true,
                theme: "dark",
                transition: Bounce,
            });
            if (error instanceof Error) {
                setErrorMessage(error.message)
            }
        }
    }

    // ...页面渲染部分
}

额外建议:在loginUser.rejected的case中把错误信息存入Redux状态,组件通过useAppSelector获取并展示,让错误处理逻辑更统一。


内容的提问来源于stack exchange,提问作者Crade47

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.29 21:57:02