在 React 开发中,useContext 和 useReducer 是两个非常强大的 Hook,可以帮助我们高效管理组件状态,尤其是在多层嵌套组件中。然而,随着项目的复杂性增加,如何合理地使用这两个 Hook 成为开发中的一个挑战。特别是在结合 TypeScript 时,类型推导和状态管理会带来一些额外的复杂性。
本文将从以下几个方面详细介绍如何在 React + TypeScript 项目中规范地使用 createContext 和 useReducer,提高代码的可维护性、可扩展性,并减少潜在的 Bug。
理解 createContext 和 useReducer
在 React 中,createContext 用于创建一个全局可访问的上下文,而 useReducer 则是管理复杂状态逻辑的一种 Hook。两者结合使用,可以解决组件之间的状态共享问题,特别是当你的状态逻辑较为复杂时,useReducer 能够让状态变更更加可控。
为 Context 提供类型支持
在使用 createContext 时,TypeScript 可以为我们提供类型检查。但如果不小心,可能会导致类型不明确的问题。我们需要确保为 Context 和其 Provider 提供正确的类型
1.定义ActionType 和 reducer
在使用 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 函数,它接收 state 和 action,根据不同的 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.ts、initialState.ts、index.tsx等,定义了全局状态、action类型、reducer以及createContext。index.tsx:应用的入口文件,通过StateProvider提供全局状态,并渲染组件。
通过这种方式,你的应用能够将状态和逻辑集中管理,使得组件间的状态共享更加清晰、简单且高效,同时借助 TypeScript 提供的类型检查增强了代码的可维护性和可扩展性。