最近遇到一个页面需要在多个组件之间共享状态,虽然使用useState
和Context
可以实现,但当状态逻辑变得复杂时,代码会变得难以维护。于是使用useReducer
和Context
来管理共享状态
useReducer 的基本用法
useReducer
是 React 提供的一个 Hook,用于在函数组件中管理复杂的状态逻辑。它类似于 Redux 中的 reducer,但更轻量。
核心概念:
-
reducer:处理状态变更的函数
(state, action) => newState
-
initialState:初始状态值
-
initFunction(可选):惰性初始化函数
-
dispatch:触发状态变更的函数
-
useReducer:接受 reducer 和初始状态,返回当前状态和 dispatch 函数
tsx
function reducer(state, action) {
switch (action.type) {
case 'ADD':
return { ...state, count: state.count + action.payload };
case 'MINUS':
return { ...state, count: state.count - action.payload };
case 'RESET':
return initialState;
default:
throw new Error('未知action类型');
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<>
Count: {state.count}
<button onClick={() => dispatch({ type: 'ADD' })}>+</button>
<button onClick={() => dispatch({ type: 'MINUS' })}>-</button>
</>
);
}
如果是 state 的话,每次变化都是 setState,逻辑相对分散,如果是多个组件中的话维护起来就会很麻烦。
tsx
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const initialState = 0;
const handleAdd = (payload) => {
setCount((prev) => prev + payload);
};
const handleReset = () => {
setCount(initialState);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={() => handleAdd(1)}>Add 1</button>
<button onClick={handleReset}>Reset</button>
</div>
);
}
使用 Context 共享状态
Context 是 React 提供的跨组件数据共享方案,解决 props 逐层传递的"prop drilling"问题。它包含三个关键部分:
- Context 对象(创建数据源)
- Provider 组件(提供数据)
- Consumer 组件/useContext Hook(消费数据)
第一步:创建 Context 对象
tsx
import { createContext } from 'react';
export const ThemeContext = createContext('light'); // 默认值
第二步:使用 Provider 提供数据
tsx
import { ThemeContext } from './ThemeContext';
function Any() {
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
第三步:使用 Consumer 或 useContext Hook 消费数据
tsx
import { useContext } from 'react';
import { ThemeContext } from './ThemeContext';
function Toolbar() {
const theme = useContext(ThemeContext);
return <div className={`toolbar ${theme}`}>Toolbar</div>;
}
结合 useReducer 和 Context
将 useReducer
和 Context
结合使用,可以在多个组件之间共享复杂状态,同时保持代码的清晰和可维护性。
以当下的需求为例,有个错题集的页面,其中包含选中错题集的数据,该数据在页面的不同子组件中,进行增删改查以及移动等操作。使用 useReducer
可以集中管理这些状态逻辑,而 Context
则可以让这些状态在组件树中共享。
第一步:创建文件 context.tsx
首先,创建文件 context.tsx
,主要定义initialState
、reducer
函数和WrongContext
:
tsx
import type {
QsSubmitItem,
QuestionItem,
} from '@/pages/WrongQuestionsSet/typing';
import { Dispatch, createContext, useContext } from 'react';
// 初始状态
export const initialState: State = {
selectedIds: [],
selectedList: [],
};
/**
* 状态reducer函数
* @param state 当前状态
* @param action 操作指令
* @returns 新状态
*/
export function reducer(state: State, action: Action): State {
const { type, payload } = action;
switch (type) {
case 'clear':
return { ...initialState };
case 'up': {
const upId: string = payload;
const upIndex = state.selectedIds.indexOf(upId);
if (upIndex <= 0) return state;
const newSelectedIds = [...state.selectedIds];
const newSelectedList = [...state.selectedList];
// 交换位置
[newSelectedIds[upIndex - 1], newSelectedIds[upIndex]] = [
newSelectedIds[upIndex],
newSelectedIds[upIndex - 1],
];
[newSelectedList[upIndex - 1], newSelectedList[upIndex]] = [
newSelectedList[upIndex],
newSelectedList[upIndex - 1],
];
return { selectedIds: newSelectedIds, selectedList: newSelectedList };
}
case 'down': {
const downId: string = payload;
const downIndex = state.selectedIds.indexOf(downId);
if (downIndex >= state.selectedIds.length - 1) return state;
const newSelectedIds = [...state.selectedIds];
const newSelectedList = [...state.selectedList];
// 交换位置
[newSelectedIds[downIndex], newSelectedIds[downIndex + 1]] = [
newSelectedIds[downIndex + 1],
newSelectedIds[downIndex],
];
[newSelectedList[downIndex], newSelectedList[downIndex + 1]] = [
newSelectedList[downIndex + 1],
newSelectedList[downIndex],
];
return { selectedIds: newSelectedIds, selectedList: newSelectedList };
}
case 'delete': {
const idToDelete: string = payload;
return {
selectedIds: state.selectedIds.filter((id) => id !== idToDelete),
selectedList: state.selectedList.filter(
(item) => item.questionId !== idToDelete,
),
};
}
case 'selectSingle': {
const { selectedKeys, selectedRows } = payload as {
selectedKeys: string[];
selectedRows: QuestionItem[];
};
const isCancelCheck = selectedKeys.length < state.selectedIds.length;
if (isCancelCheck) {
// 取消勾选时直接过滤已选列表
return {
selectedIds: selectedKeys,
selectedList: selectedKeys.map(
(id) =>
state.selectedList.find(
(qsItem) => qsItem.questionId === id,
) as QsSubmitItem,
),
};
}
// 新增勾选时合并列表
const newItem = selectedRows[selectedRows.length - 1];
return {
selectedIds: selectedKeys,
selectedList: [...state.selectedList, ...formatQsSubmitList([newItem])],
};
}
case 'selectQueryAll': {
const queryAllRows: QsSubmitItem[] = payload;
const allSelectedIds = Array.from(
new Set([
...state.selectedIds,
...queryAllRows.map((item) => item.questionId),
]),
);
const allSelectedList = allSelectedIds.map(
(id) =>
[...state.selectedList, ...queryAllRows].find(
(item) => item.questionId === id,
) as QsSubmitItem,
);
// 限制最大选择数量为1000
return {
selectedIds: allSelectedIds.slice(0, 1000),
selectedList: allSelectedList.slice(0, 1000),
};
}
case 'cancelSelectQueryAll': {
const curAllIds: string[] = payload;
const newSelectedIds = state.selectedIds.filter(
(id) => !curAllIds.includes(id),
);
return {
selectedIds: newSelectedIds,
selectedList: newSelectedIds.map(
(id) =>
state.selectedList.find(
(item) => item.questionId === id,
) as QsSubmitItem,
),
};
}
default:
throw new Error(`Unknown action type: ${type}`);
}
}
// 创建上下文
export const WrongContext = createContext<WrongContextType | undefined>(
undefined,
);
/**
* 自定义hook用于访问错题集上下文
* @throws 如果不在WrongProvider中使用会抛出错误
* @returns 上下文对象
*/
export function useWrongContext() {
const context = useContext(WrongContext);
if (!context) {
throw new Error('useWrongContext must be used within a WrongProvider');
}
return context;
}
/**
* 格式化题目列表为提交格式
* @param qsList 原始题目列表
* @returns 格式化后的提交列表
*/
function formatQsSubmitList(qsList: QuestionItem[]): QsSubmitItem[] {
return qsList.map(({ questionId, answerTimes, rightRatio }) => ({
questionId,
answerTimes,
rightRatio,
}));
}
// 状态类型定义
type State = {
selectedIds: string[]; // 已选题目ID数组
selectedList: QsSubmitItem[]; // 已选题目详情列表
};
// 操作类型定义
type ActionType =
| 'clear' // 清空选择
| 'selectSingle' // 单选操作
| 'delete' // 删除单个
| 'up' // 上移
| 'down' // 下移
| 'selectQueryAll' // 全选当前查询
| 'cancelSelectQueryAll'; // 取消全选当前查询
type Action = {
type: ActionType;
payload?: any; // 操作负载数据
};
// 上下文类型定义
type WrongContextType = {
state: State;
dispatch: Dispatch<Action>;
};
第二步:在页面中使用 useReducer
和 Context
接下来,在页面中使用 useReducer
和 Context
来管理和共享状态。
tsx
import { useReducer } from 'react';
import { WrongContext, initialState, reducer } from './WrongContext';
export function PageXX() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<WrongContext.Provider value={{ state, dispatch }}>
<div>错题个数 {state.selectedIds.length}</div>
{/* 其他组件可以在这里使用 useWrongContext 获取状态和 dispatch */}
<Button onClick={dispatch({ type: 'clear' })}>清除</Button>
<PreviewWrongList />
<ExportWrongList />
</WrongContext.Provider>
);
}
第三步:在子组件中使用 useWrongContext
获取状态和 dispatch
tsx
// PreviewWrongList.tsx
import { useWrongContext } from './WrongContext';
const PreviewWrongList = () => {
const { state, dispatch } = useWrongContext();
return (
<div>
<h2>预览错题,共{state.selectedIds.length}个</h2>
<button onClick={dispatch({ type: 'delete', 'xxId' })}>删除</button>
<button onClick={dispatch({ type: 'up', 'xxId' })}>上移</button>
<button onClick={dispatch({ type: 'down', 'xxId' })}>下移</button>
</div>
);
};
这样就完成了使用 useReducer
和 Context
在 React 中共享状态的基本实现。通过这种方式,可以将复杂的状态逻辑集中在一个地方,避免了多个组件之间状态管理的混乱。如果使用传统的 useState
和 props 传递方式,状态修改变得分散且难以追踪。
总结
使用 useReducer
和 Context
可以有效地管理和共享复杂状态,尤其是在多个组件之间需要共享数据时。
相比于 useState
和 props 逐层传递的方式,这种方法更清晰、更易于维护。这种模式在大型应用中尤其有用,可以帮助我们更好地组织代码和管理状态。希望这个示例能对你在 React 中的状态管理有所帮助!