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

如何在Axios+Zod封装的客户端类中使用Redux dispatch?

问题描述

我基于Axios、Zod和Redux开发了一个请求客户端封装器,已经实现了Zod响应校验,现在想把请求的成功/错误状态统一存入Redux,但尝试多种方式把dispatch集成到类里都没生效。

当前未集成Redux的客户端代码:

interface RequestConfig<T extends z.ZodTypeAny> extends AxiosRequestConfig {
  responseSchema: T;
}

export class ValidatedAxiosClient {
  delete = this.createValidatedAxiosRequest('DELETE');
  get = this.createValidatedAxiosRequest('GET');
  patch = this.createValidatedAxiosRequest('PATCH');
  put = this.createValidatedAxiosRequest('PUT');
  post = this.createValidatedAxiosRequest('POST');

  private unableToFetch = 'error:unableToFetch';

  constructor(private service: string, private baseUrl: string) {}

  private handleNetworkError<T extends z.ZodTypeAny>(
    error: HttpError,
    url: string,
    config: RequestConfig<T>
  ) {
    /**
     * Handle error...
     */
  }

  private handleZodError<T extends z.ZodTypeAny>(
    error: z.ZodError,
    url: string,
    config: RequestConfig<T>
  ) {
    /**
     * Parse and handle ZodError...
     */
  }

  private createValidatedAxiosRequest(method: HTTPMethod) {
    return async <T extends z.ZodTypeAny>(
      url: string,
      config: RequestConfig<T>
    ): Promise<z.infer<T>> => {
      try {
        const response = await axios({ ...config, method, url, baseURL: this.baseUrl });
        return config.responseSchema.parse(response);
      } catch (error: any) {
        if (error instanceof z.ZodError) {
          console.log(JSON.stringify({ error }, undefined, 4));
          this.handleZodError<T>(error, url, config);
        }
        if (error instanceof HttpError) {
          this.handleNetworkError<T>(error, url, config);
        }
      }
    };
  }
}

当前的Thunk使用方式:

const WrappedClient = new ValidatedAxiosClient('UserService', 'http://localhost:3000')

export const fetchUser =
  (userId: string): ThunkAction<void, RootState, unknown, Action<string>> =>
  async (dispatch, getState) => {
    if (getState().users.status === 'pending') return;

    dispatch(startedUsersLoading());

    const { name, age } = await WrappedClient.get('/user', {
      params: { userId },
      responseSchema: z.object({
        name: z.string(),
        age: z.number()
      })
    });

    dispatch(getUsersSuccess());
  }

现在的问题是每个Thunk里都要写加载/成功/错误的dispatch逻辑,多服务场景下重复代码太多,想把这部分逻辑移入客户端类,但尝试过两种方式都不行:

  • 把dispatch作为参数传入请求方法,但dispatch从未被调用
  • 把dispatch传入构造函数,但每次调用Thunk都要实例化新客户端,不符合需求

有没有可行的实现方式?

可行解决方案

方案1:给客户端类添加setDispatch方法

在客户端类中添加可动态设置的dispatch属性,初始化时不需要传入,在应用启动或首次调用Thunk时设置一次,后续复用同一个客户端实例。

修改后的客户端代码:

import { Dispatch } from '@reduxjs/toolkit';
import { Action } from 'redux';

interface RequestConfig<T extends z.ZodTypeAny> extends AxiosRequestConfig {
  responseSchema: T;
}

export class ValidatedAxiosClient {
  delete = this.createValidatedAxiosRequest('DELETE');
  get = this.createValidatedAxiosRequest('GET');
  patch = this.createValidatedAxiosRequest('PATCH');
  put = this.createValidatedAxiosRequest('PUT');
  post = this.createValidatedAxiosRequest('POST');

  private unableToFetch = 'error:unableToFetch';
  private dispatch?: Dispatch<Action>; // 新增dispatch属性

  constructor(private service: string, private baseUrl: string) {}

  // 新增设置dispatch的方法
  setDispatch(dispatch: Dispatch<Action>) {
    this.dispatch = dispatch;
  }

  private handleNetworkError<T extends z.ZodTypeAny>(
    error: HttpError,
    url: string,
    config: RequestConfig<T>
  ) {
    // 分发网络错误状态
    this.dispatch?.dispatch({
      type: `${this.service}/error`,
      payload: { message: error.message, url }
    });
    // 原有错误处理逻辑
  }

  private handleZodError<T extends z.ZodTypeAny>(
    error: z.ZodError,
    url: string,
    config: RequestConfig<T>
  ) {
    // 分发Zod校验错误状态
    this.dispatch?.dispatch({
      type: `${this.service}/validationError`,
      payload: { errors: error.errors, url }
    });
    // 原有错误处理逻辑
  }

  private createValidatedAxiosRequest(method: HTTPMethod) {
    return async <T extends z.ZodTypeAny>(
      url: string,
      config: RequestConfig<T>
    ): Promise<z.infer<T>> => {
      try {
        // 分发加载中状态
        this.dispatch?.dispatch({
          type: `${this.service}/loading`,
          payload: { url }
        });

        const response = await axios({ ...config, method, url, baseURL: this.baseUrl });
        const parsedData = config.responseSchema.parse(response.data); // 修正:解析响应体data而非整个response对象

        // 分发成功状态
        this.dispatch?.dispatch({
          type: `${this.service}/success`,
          payload: { data: parsedData, url }
        });

        return parsedData;
      } catch (error: any) {
        if (error instanceof z.ZodError) {
          this.handleZodError<T>(error, url, config);
        } else if (error instanceof HttpError) {
          this.handleNetworkError<T>(error, url, config);
        } else {
          // 处理未知错误
          this.dispatch?.dispatch({
            type: `${this.service}/unknownError`,
            payload: { message: error.message, url }
          });
        }
        throw error; // 抛出错误让Thunk可按需处理
      }
    };
  }
}

使用方式:

// 全局初始化客户端实例
const WrappedClient = new ValidatedAxiosClient('UserService', 'http://localhost:3000');

export const fetchUser =
  (userId: string): ThunkAction<void, RootState, unknown, Action<string>> =>
  async (dispatch, getState) => {
    if (getState().users.status === 'pending') return;

    // 首次调用时设置dispatch
    if (!WrappedClient['dispatch']) {
      WrappedClient.setDispatch(dispatch);
    }

    try {
      const { name, age } = await WrappedClient.get('/user', {
        params: { userId },
        responseSchema: z.object({
          name: z.string(),
          age: z.number()
        })
      });
      // 如需额外成功逻辑,在此补充
    } catch (error) {
      // 如需额外错误处理,在此补充
    }
  }

方案2:将dispatch作为可选参数传入请求方法

修改请求方法的参数,允许传入dispatch,既可以在Thunk中传入以更新Redux状态,也可以在非Redux场景下调用时不传。

修改后的createValidatedAxiosRequest方法:

private createValidatedAxiosRequest(method: HTTPMethod) {
  return async <T extends z.ZodTypeAny>(
    url: string,
    config: RequestConfig<T>,
    dispatch?: Dispatch<Action> // 新增可选dispatch参数
  ): Promise<z.infer<T>> => {
    try {
      dispatch?.dispatch({
        type: `${this.service}/loading`,
        payload: { url }
      });

      const response = await axios({ ...config, method, url, baseURL: this.baseUrl });
      const parsedData = config.responseSchema.parse(response.data);

      dispatch?.dispatch({
        type: `${this.service}/success`,
        payload: { data: parsedData, url }
      });

      return parsedData;
    } catch (error: any) {
      if (error instanceof z.ZodError) {
        dispatch?.dispatch({
          type: `${this.service}/validationError`,
          payload: { errors: error.errors, url }
        });
        this.handleZodError<T>(error, url, config);
      } else if (error instanceof HttpError) {
        dispatch?.dispatch({
          type: `${this.service}/error`,
          payload: { message: error.message, url }
        });
        this.handleNetworkError<T>(error, url, config);
      } else {
        dispatch?.dispatch({
          type: `${this.service}/unknownError`,
          payload: { message: error.message, url }
        });
      }
      throw error;
    }
  };
}

使用方式:

const WrappedClient = new ValidatedAxiosClient('UserService', 'http://localhost:3000')

export const fetchUser =
  (userId: string): ThunkAction<void, RootState, unknown, Action<string>> =>
  async (dispatch, getState) => {
    if (getState().users.status === 'pending') return;

    try {
      const { name, age } = await WrappedClient.get('/user', {
        params: { userId },
        responseSchema: z.object({
          name: z.string(),
          age: z.number()
        })
      }, dispatch); // 传入dispatch
    } catch (error) {
      // 处理额外逻辑
    }
  }

方案3:使用Redux Toolkit的createApi(推荐)

如果项目已经使用Redux Toolkit,推荐直接用createApi(RTK Query),它内置了请求状态管理(加载、成功、错误)、缓存、自动重发等功能,完全可以替代手动封装的Axios客户端,大幅减少重复代码。

示例:

import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
import z from 'zod';

const userApi = createApi({
  reducerPath: 'userApi',
  baseQuery: fetchBaseQuery({ baseUrl: 'http://localhost:3000' }),
  endpoints: (builder) => ({
    fetchUser: builder.query({
      query: (userId) => `/user?userId=${userId}`,
      // 使用Zod校验响应
      transformResponse: (response: unknown) => {
        const schema = z.object({
          name: z.string(),
          age: z.number()
        });
        return schema.parse(response);
      },
    }),
  }),
});

export const { useFetchUserQuery } = userApi;

使用时直接调用Hook,RTK Query会自动管理状态:

function UserComponent({ userId }) {
  const { data, error, isLoading } = useFetchUserQuery(userId);

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return <div>Name: {data.name}, Age: {data.age}</div>;
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.20 13:14:55