如何在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
相关产品推荐
相关产品推荐

