You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何模块化自定义React状态容器Hook并保留严格TypeScript类型约束

解决方案

核心实现思路

我们采用工厂函数替代你构思的类式API,更适配TypeScript的类型推导逻辑,不需要用户手动声明任何额外类型,所有约束和原有实现完全一致,同时彻底消除样板代码重复问题。

通用包实现代码(@company/react-state-container)

import React, { useReducer, useContext, ReactNode, createContext } from "react";
import _cloneDeep from "lodash/cloneDeep";

// 内部工具类型,推导动作参数类型
type ActionParams<H> = H extends (state: any, ...args: infer P) => any ? P : never;

export function createStateContainer<S extends Record<string, any>, H extends Record<string, (state: S, ...args: any[]) => Partial<S> | void>>(
  hookName: string,
  config: {
    initialState: S;
    actionHandlers: H;
  }
) {
  const { initialState, actionHandlers } = config;

  // 推导动作类型
  type Actions = {
    [K in keyof H]: ActionParams<H[K]> extends [infer D] ? D : null;
  };

  type Dispatch = <T extends keyof H>(
    type: T,
    ...data: ActionParams<H[T]>
  ) => void;

  type ActionsMap = {
    [K in keyof Actions]: Actions[K] extends null 
      ? { type: K, data?: undefined } 
      : { type: K, data: Actions[K] }
  };
  type Action = ActionsMap[keyof Actions];

  const Context = createContext<{
    state: S;
    dispatch: Dispatch;
  } | undefined>(undefined);

  function reducer(state: S, action: Action): S {
    const stateClone = _cloneDeep(state);
    // @ts-ignore 内部逻辑已保证类型安全
    const newState = actionHandlers[action.type](stateClone, action.data as any);
    if (!newState) return state;
    return { ...stateClone, ...newState };
  }

  function Provider({ children, defaultState }: { children: ReactNode, defaultState?: S }) {
    const [state, reducerDispatch] = useReducer(reducer, defaultState ?? initialState);
    const dispatch: Dispatch = (type, ...data) => {
      // @ts-ignore
      reducerDispatch({ type, data: data[0] } as Action);
    };
    return (
      <Context.Provider value={{ state, dispatch }}>
        {children}
      </Context.Provider>
    );
  }

  function useStateContainer() {
    const context = useContext(Context);
    if (context === undefined) {
      throw new Error(`use${hookName} must be used within a ${hookName}Provider`);
    }
    return context;
  }

  return {
    Provider,
    useStateContainer
  };
}

业务侧使用方式

import { createStateContainer } from "@company/react-state-container";

const {
  Provider: MyStateProvider,
  useStateContainer: useMyState
} = createStateContainer("MyState", {
  initialState: {
    nums: [] as number[]
  },
  actionHandlers: {
    RESET_NUMS: () => ({ nums: [] }),
    ADD_NUM: ({ nums }, num: number) => {
      nums.push(num);
      return { nums };
    },
    SET_NUMS: ({}, nums: number[]) => ({ nums })
  }
});

export { MyStateProvider, useMyState };

特性匹配说明

所有原有类型约束完全保留:

  • initialState类型自动推导,不需要手动声明State类型
  • dispatch调用严格校验:仅支持传入已定义的动作名,参数数量、类型完全匹配动作处理函数的定义,无参数的动作不允许传参
  • 动作处理函数的入参类型自动推导,状态参数类型和initialState完全一致,动作参数类型由用户在处理函数中定义一次即可全局生效
  • 无重复样板代码:后续需要调整底层实现(比如替换深拷贝实现、调整错误提示逻辑)仅需修改通用包,所有业务侧的状态容器自动同步更新

如果你坚持要用之前构思的链式调用API,也可以通过TS的方法泛型链式推导实现,不过工厂函数的方式代码更简洁,类型推导逻辑更稳定,没有边界case问题。

内容的提问来源于stack exchange,提问作者ChevCast

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.04 04:36:02