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
相关产品推荐
相关产品推荐

