使用useReducer和Context进行React中的页面内部数据共享

最近遇到一个页面需要在多个组件之间共享状态,虽然使用useStateContext可以实现,但当状态逻辑变得复杂时,代码会变得难以维护。于是使用useReducerContext来管理共享状态

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

useReducerContext 结合使用,可以在多个组件之间共享复杂状态,同时保持代码的清晰和可维护性。

以当下的需求为例,有个错题集的页面,其中包含选中错题集的数据,该数据在页面的不同子组件中,进行增删改查以及移动等操作。使用 useReducer 可以集中管理这些状态逻辑,而 Context 则可以让这些状态在组件树中共享。

第一步:创建文件 context.tsx

首先,创建文件 context.tsx,主要定义initialStatereducer函数和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>;
};

第二步:在页面中使用 useReducerContext

接下来,在页面中使用 useReducerContext 来管理和共享状态。

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>
  );
};

这样就完成了使用 useReducerContext 在 React 中共享状态的基本实现。通过这种方式,可以将复杂的状态逻辑集中在一个地方,避免了多个组件之间状态管理的混乱。如果使用传统的 useState 和 props 传递方式,状态修改变得分散且难以追踪。

总结

使用 useReducerContext 可以有效地管理和共享复杂状态,尤其是在多个组件之间需要共享数据时。

相比于 useState 和 props 逐层传递的方式,这种方法更清晰、更易于维护。这种模式在大型应用中尤其有用,可以帮助我们更好地组织代码和管理状态。希望这个示例能对你在 React 中的状态管理有所帮助!

参考资料

相关推荐
喝拿铁写前端1 分钟前
技术是决策与代价的平衡 —— 超大系统从 Vue 2 向 Vue 3 演进的思考
前端·vue.js·架构
拾光拾趣录8 分钟前
虚拟滚动 + 加载:让万级列表丝般顺滑
前端·javascript
然我17 分钟前
数组的创建与遍历:从入门到精通,这些坑你踩过吗? 🧐
前端·javascript·面试
豆豆(设计前端)24 分钟前
如何成为高级前端开发者:系统化成长路径。
前端·javascript·vue.js·面试·electron
今天你写算法了吗29 分钟前
ScratchCard刮刮卡交互元素的实现
前端·javascript
谢尔登41 分钟前
【React Native】布局和 Stack 、Slot
javascript·react native·react.js
FogLetter1 小时前
深入浅出 JavaScript 数组:从基础到高级玩法
前端·javascript
一小池勺1 小时前
🚀 clsx vs shadcn/ui的cn函数:前端类名拼接工具大PK
前端
lens941 小时前
RSC、SSR傻傻分不清?一文搞懂所有渲染概念!
前端·next.js
spionbo1 小时前
前端部署VuePress Theme Hope主题部署到gitlab,使用pnpm构建,再同步到netlify绑定腾讯云域名实现
前端