吐司问卷:Redux 状态管理
Date: February 18, 2025 5:37 PM (GMT+8)
Redux 管理用户信息
命名规范:
以 Info 结尾表示获取Reudx信息,比如 useGetUserInfo.ts
以 data 结尾表示获取服务端信息,比如 useLoadQuestionData
采用 Redux 管理用户信息
Redux store 设计:
src/store/index.ts
import { configureStore } from '@reduxjs/toolkit'
import userReducer, { UserStateType } from './userReducer'
export type StateType = {
user: UserStateType
}
export default configureStore({
reducer: {
user: userReducer,
},
})
userReducer 开发:
src/store/userReducer.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
export type UserStateType = {
username: string
nickname: string
}
const INIT_STATE: UserStateType = {
username: '',
nickname: '',
}
export const userSlice = createSlice({
name: 'user',
initialState: INIT_STATE,
reducers: {
loginReducer: (
state: UserStateType,
action: PayloadAction<UserStateType>
) => {
return action.payload
},
logoutReducer: () => {
return INIT_STATE
},
},
})
export const { loginReducer, logoutReducer } = userSlice.actions
export default userSlice.reducer
src/index.ts
import React from 'react'
import ReactDOM from 'react-dom/client'
+import { Provider } from 'react-redux'
+import store from './store'
import App from './App'
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)
root.render(
<React.StrictMode>
- <App />
+ <Provider store={store}>
+ <App />
+ </Provider>
</React.StrictMode>
)
点击Logo跳转优化
**需求:**原本点击 Logo 会跳转到 home 页。现在需要根据是否有用户信息,进行区分判断。
如果有用户信息,点击 Logo 则跳转到管理页面。没有则到 home 页面。
src/component/Logo.tsx
-import React, { FC } from 'react'
+import React, { FC, useState, useEffect } from 'react'
import { Space, Typography } from 'antd'
import { BlockOutlined } from '@ant-design/icons'
import { Link } from 'react-router-dom'
+import useGetUserInfo from '../hooks/useGetUserInfo'
+import { HOME_PATHNAME, MANAGE_INDEX_PATHNAME } from '../router/index'
import styles from './Logo.module.scss'
const { Title } = Typography
const Logo: FC = () => {
+ const { username } = useGetUserInfo()
+ const [pathname, setPathname] = useState<string>(HOME_PATHNAME)
+ useEffect(() => {
+ if (username) {
+ setPathname(MANAGE_INDEX_PATHNAME)
+ }
+ }, [username])
return (
<div className={styles.container}>
- <Link to="/">
+ <Link to={pathname}>
<Space>
<Title>
<BlockOutlined />
自定义 Hook 统一加载用户信息
思路:
- 开发获取 Redux 中用户信息
- 开发获取 服务端 用户信息,并与 Redux 联动
- 设计页面加载状态(根据用户信息获取与否进行判断)
- 有用户信息,则停止加载。没有则进行加载。
useGetUserInfo.tsx
import { useSelector } from 'react-redux'
import { StateType } from '../store'
import { UserStateType } from '../store/userReducer'
function useGetUserInfo() {
const { username, nickname } = useSelector<StateType>(
(state: StateType) => state.user
) as UserStateType
return { username, nickname }
}
export default useGetUserInfo
useLoadUserData.ts
import { useEffect, useState } from 'react'
import { useRequest } from 'ahooks'
import { getUserInfoService } from '../services/user'
import { useDispatch } from 'react-redux'
import useGetUserInfo from './useGetUserInfo'
import { loginReducer } from '../store/userReducer'
function useLoadUserData() {
const [waitingUserData, setWaitingUserData] = useState<boolean>(false)
const dispatch = useDispatch()
// 加载用户信息
const { run } = useRequest(getUserInfoService, {
manual: true,
onSuccess: res => {
const { username, nickname } = res
// 存储到 Redux 中
dispatch(loginReducer({ username, nickname }))
},
onFinally() {
setWaitingUserData(false)
},
})
// Redux 中数据
const { username } = useGetUserInfo()
useEffect(() => {
if (username) {
setWaitingUserData(false)
}
run()
}, [username])
return { waitingUserData }
}
export default useLoadUserData
用户退出功能优化
**思路:**用户退出不仅需要清空 Redux 信息,还要清空本地 token 信息
src/component/userInfo.tsx
import { LOGIN_PATHNAME } from '../router'
import { Button, message } from 'antd'
import { UserOutlined } from '@ant-design/icons'
-import { getUserInfoService } from '../services/user'
import { removeToken } from '../utils/user-token'
-import { useRequest } from 'ahooks'
+import useGetUserInfo from '../hooks/useGetUserInfo'
+import { useDispatch } from 'react-redux'
+import { logoutReducer } from '../store/userReducer'
const UserInfo: FC = () => {
const nav = useNavigate()
- const { data } = useRequest(getUserInfoService)
- const { username, nickname } = data || {}
+ const { username, nickname } = useGetUserInfo()
+ const dispatch = useDispatch()
const logout = () => {
- removeToken()
+ dispatch(logoutReducer()) // 清空 redux user 中的数据
+ removeToken() // 清空 token 的存储
message.success('退出成功')
nav(LOGIN_PATHNAME)
}
根据用户登录状态动态跳转页面
需求:
- 当用户已经登陆,在问卷管理页面时,URL中输 /login,会自动返回问卷管理页面,而非再登陆
实现思路:
- 设计 useNavPage 钩子函数:获取用户状态与URL,根据用户状态判断是否页面跳转走向。
useNavPage.ts
import { useEffect } from 'react'
import { useNavigate, useLocation } from 'react-router-dom'
import {
isLoginOrRegister,
isNoNeedUserInfo,
MANAGE_INDEX_PATHNAME,
LOGIN_PATHNAME,
} from '../router'
import useGetUserInfo from './useGetUserInfo'
function useNavPage(waitingUserData: boolean) {
const { username } = useGetUserInfo()
const { pathname } = useLocation()
const nav = useNavigate()
useEffect(() => {
if (waitingUserData) return
if (username) {
if (isLoginOrRegister(pathname)) {
nav(MANAGE_INDEX_PATHNAME)
}
return
}
if (isNoNeedUserInfo(pathname)) {
return
} else {
nav(LOGIN_PATHNAME)
}
}, [waitingUserData, username, pathname])
}
export default useNavPage
src/router/index.ts
export function isLoginOrRegister(pathname: string) {
return [LOGIN_PATHNAME, REGISTER_PATHNAME].includes(pathname)
}
export function isNoNeedUserInfo(pathname: string) {
return [HOME_PATHNAME, LOGIN_PATHNAME, REGISTER_PATHNAME].includes(pathname)
}
Bugfix
Q:请求不断发送
项目中有一处 请求会不断发送,在 Chrome Network 中可以看出。
useLoadUserData.ts
useEffect(() => {
if (username) {
setWaitingUserData(false)
}
run() // 这里可能造成循环调用
}, [username])
问题原因:
- 当Redux中的
username
为空时,run()
会执行获取用户信息 - 获取成功后通过
dispatch(loginReducer)
更新username - username更新触发useEffect再次执行
- 形成「获取数据 → 更新username → 触发请求」的循环
解决方案:
修改useLoadUserData.ts中的useEffect:
useEffect(() => {
// 当已有用户信息时不再请求
if (username) {
setWaitingUserData(false)
return
}
run()
}, [username]) // 保持原有依赖
Q:页面刷新自动弹回登陆界面
问题原因分析:
- 在
useLoadUserData
hook中,用户信息加载完成后才会设置waitingUserData
为false useNavPage
hook中会在用户信息未加载完成时判断路由权限- 页面刷新时会清空Redux状态,导致需要重新加载用户信息
- 用户信息加载是异步操作,在加载完成前路由守卫已执行跳转判断
解决方案: 修改useLoadUserData.ts
文件,在组件挂载时立即触发用户信息加载:
useLoadUserData.ts
function useLoadUserData() {
const [waitingUserData, setWaitingUserData] = useState<boolean>(true) // 初始值改为 true
const dispatch = useDispatch()
// 加载用户信息
const { run } = useRequest(getUserInfoService, {
manual: true,
onSuccess: res => {
const { username, nickname } = res
dispatch(loginReducer({ username, nickname }))
},
onFinally() {
setWaitingUserData(false)
},
})
// 添加 useEffect 立即执行数据加载
useEffect(() => {
run()
}, [])
// Redux 中数据
const { username } = useGetUserInfo()
useEffect(() => {
if (username) {
setWaitingUserData(false)
}
}, [username])
return { waitingUserData }
}
修改说明:
- 将
waitingUserData
初始值设为true
,确保加载状态初始正确 - 添加立即执行的
useEffect
触发用户信息加载 - 保留原有的username依赖检查作为双重保障
同时需要确保在utils/user-token.ts
中正确实现了token的持久化存储(根据代码片段推断应已实现)。
这个修改可以保证:
- 页面刷新时立即触发用户信息加载
- 在用户信息加载完成前保持等待状态
- 路由守卫(
useNavPage
)会等待用户信息加载完成后再进行跳转判断 - 避免在异步加载过程中误判未登录状态