React 中 createContext 搭配 useReducer 的实战技巧与最佳实践

649 阅读4分钟

在 React 开发中,useContextuseReducer 是两个非常强大的 Hook,可以帮助我们高效管理组件状态,尤其是在多层嵌套组件中。然而,随着项目的复杂性增加,如何合理地使用这两个 Hook 成为开发中的一个挑战。特别是在结合 TypeScript 时,类型推导和状态管理会带来一些额外的复杂性。

本文将从以下几个方面详细介绍如何在 React + TypeScript 项目中规范地使用 createContextuseReducer,提高代码的可维护性、可扩展性,并减少潜在的 Bug。

理解 createContextuseReducer

在 React 中,createContext 用于创建一个全局可访问的上下文,而 useReducer 则是管理复杂状态逻辑的一种 Hook。两者结合使用,可以解决组件之间的状态共享问题,特别是当你的状态逻辑较为复杂时,useReducer 能够让状态变更更加可控。

为 Context 提供类型支持

在使用 createContext 时,TypeScript 可以为我们提供类型检查。但如果不小心,可能会导致类型不明确的问题。我们需要确保为 Context 和其 Provider 提供正确的类型

1.定义ActionTypereducer

在使用 useReducer 时,首先需要定义各种可能的动作类型(ActionType)。这些动作类型是对状态的具体操作,并且它们的结构应尽可能简洁和一致。以下是我们在项目中使用的 ActionType,它定义了多个与项目管理相关的状态更新操作。

/**
 * State
 * @description 事件类型
 */
export const ActionType = Object.freeze({
  SetProjects: 'SET_PROJECTS', // 设置项目列表列表
  SetIdConfig: 'SET_ID_CONFIG', // 设置ID配置
  SetProjectsLoading: 'SET_PROJECTS_LOADING', // 设置项目列表加载状态
});

定义初始状态数据


/**
 * State
 * @description 页面容器初始状态
 */
export const initialState = {
  loading: false,
  appId: '', // 应用id
  groupId: '', // 分组id
  projects: [], // 项目列表 信息
};

然后,我们定义了一个 reducer 函数,它接收 stateaction,根据不同的 action.type 来更新状态。

import { initialState } from './initialState';

const [state, dispatch] = useReducer((originalState, action) => {
    switch (action.type) {
      // 设置ID配置
      case ActionType.SetIdConfig: {
        return {
          ...originalState,
          appId: action.appId,
          groupId: action.groupId,
        };
      }
    
      // 设置项目列表加载状态
      case ActionType.SetProjectsLoading: {
        return {
          ...originalState,
          loading: action.loading,
        };
      }
      
      // 设置项目列表列表
      case ActionType.SetProjects: {
        return {
          ...originalState,
          projects: action.projects,
        };
      }

      default: {
        return { ...originalState };
      }
    }
  }, initialState);

2.创建上下文

使用 createContext 创建上下文,并导出上下文对象。

import React, { createContext, useContext, useCallback } from 'react';

/**
 * 创建上下文
 */
const StateContext = createContext();
const { Provider } = StateContext;

/**
 * 创建一个自定义 hook,便于使用上下文
 * @returns {object}
 */
export const useStateCtx = () => {
  const { state, dispatch } = useContext(StateContext);
  return { state, dispatch };
};


3.创建 Provider 组件

创建一个 Provider 组件,将状态和 dispatch 方法通过上下文提供给子组件。、

import React, { createContext, useReducer, useMemo, useContext, useCallback } from 'react';
import { initialState } from './initialState';
import { ActionType } from './actionType';

/**
 * 创建上下文 
 */
const StateContext = createContext(); 
const { Provider } = StateContext;

/**
 * Provider
 * @description 提供数据上下文
 * @returns {JSX.Element}
 */
const StateProvider = ({ children }) => {

     const [state, dispatch] = useReducer((originalState, action) => {
        switch (action.type) {
          // 设置ID配置
          case ActionType.SetIdConfig: {
            return {
              ...originalState,
              appId: action.appId,
              groupId: action.groupId,
            };
          }

          // 设置项目列表加载状态
          case ActionType.SetProjectsLoading: {
            return {
              ...originalState,
              loading: action.loading,
            };
          }

          // 设置项目列表列表
          case ActionType.SetProjects: {
            return {
              ...originalState,
              projects: action.projects,
            };
          }

          default: {
            return { ...originalState };
          }
        }
      }, initialState);

      /**
       * Callback
       * @description 获取项目列表
       * @returns {Promise<void>}
       */
      const getProjects = useCallback(
        async projectId => {
          const resp = await getProjectsAPI({ projectId });
          if (!resp.success) return;

          dispatch({
            type: ActionType.SetProjects,
            projects: resp.data.rows,
          });

          return resp.data.rows;
        },
        [dispatch],
      );

      /**
       * Memo
       * @description 上下文
       * @returns {object}
       */
      const value = useMemo(
        () => ({
          state: {
            ...state,
            getProjects,
          },
          dispatch,
        }),
        [state],
      );
 
      return <Provider value={value}>{children}</Provider>;
}

/** 
 * 创建一个自定义 hook,便于使用上下文 
 * @returns {object} 
 */ 
 export const useStateCtx = () => { 
   const { state, dispatch } = useContext(StateContext); 
   return { state, dispatch };
   
 };

4.在组件中使用上下文

在需要使用状态的组件中调用 dispatch 方法。

import React from 'react';
import { useStateCtx } from '../store';
import { ActionType } from '../store/actionType';

const Counter = () => {
  const { state, dispatch } = useStateCtx();

  return (
    <div>
      <p>Count: {state.appId}</p>
      <button onClick={() => 
        dispatch({ type: ActionType, appId:  '3598' })
      }
      >设置Appid</button>
    </div>
  );
};

5.在组件中使用 StateProvider 包裹子组件以提供状态管理

确保组件中使用 StateProvider 包裹子组件。

import React from 'react';
import { StateProvider } from './store';
import Counter from './Components/Counter';

const CustomComponent = props => {
  <StateProvider>
     <Counter />
  </StateProvider>
}

目录结构示例

为了更好地组织代码,尤其是当项目变得越来越复杂时,可以按模块拆分上下文、状态管理、Action 类型和初始状态。

复制代码
├── Components/
│   ├── Counter.tsx
├── store/
│   ├── actionType.ts
│   ├── index.tsx
│   ├── initialState.ts
├──index.tsx

总结

  • Components/ :存放所有 UI 组件(如 Counter)。
  • store/ :用于管理应用状态的文件夹,包含 actionType.tsinitialState.tsindex.tsx 等,定义了全局状态、action 类型、reducer 以及 createContext
  • index.tsx:应用的入口文件,通过 StateProvider 提供全局状态,并渲染组件。

通过这种方式,你的应用能够将状态和逻辑集中管理,使得组件间的状态共享更加清晰、简单且高效,同时借助 TypeScript 提供的类型检查增强了代码的可维护性和可扩展性。