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

参考资料

相关推荐
崔庆才丨静觅6 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60617 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了7 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅7 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅7 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅8 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment8 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅8 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊8 小时前
jwt介绍
前端
爱敲代码的小鱼8 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax