使用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 中的状态管理有所帮助!

参考资料

相关推荐
碎像16 分钟前
uni-app实战教程 从0到1开发 画图软件 (学会画图)
前端·javascript·css·程序人生·uni-app
Hilaku33 分钟前
从“高级”到“资深”,我卡了两年和我的思考
前端·javascript·面试
WebInfra1 小时前
Rsdoctor 1.2 发布:打包产物体积一目了然
前端·javascript·github
用户52709648744901 小时前
SCSS模块系统详解:@import、@use、@forward 深度解析
前端
兮漫天1 小时前
bun + vite7 的结合,孕育的 Robot Admin 【靓仔出道】(十一)
前端·vue.js
xianxin_1 小时前
CSS Text(文本)
前端
秋天的一阵风1 小时前
😈 藏在对象里的 “无限套娃”?教你一眼识破循环引用诡计!
前端·javascript·面试
电商API大数据接口开发Cris1 小时前
API 接口接入与开发演示:教你搭建淘宝商品实时数据监控
前端·数据挖掘·api
用户1409508112801 小时前
原型链、闭包、事件循环等概念,通过手写代码题验证理解深度
前端·javascript
汪子熙1 小时前
错误消息 Could not find Nx modules in this workspace 的解决办法
前端·javascript