如何合规封装React Query的useQuery?规避Hook规则冲突
正确封装React Query useQuery的解决方案
我尝试封装useQuery以统一管理其调用,但遇到问题:queryFn在运行时构建,导致自定义Hook中需根据queryFn是否就绪条件返回结果,这违反了React Hook规则。求正确封装useQuery的方案,当前代码如下:
import { QueryFunction, QueryKey, UseQueryOptions, UseQueryResult, useQuery, } from "@tanstack/react-query"; import { AxiosRequestConfig, AxiosResponse } from "axios"; import { ApiQueryConfig, QueryPathParamsType, QueryReturnType, QueryUrlParamsType, useApiClient, } from "@api"; import { combineQueryKey } from "./utils"; import { useEffect, useState } from "react"; const useApiQuery = < T extends ApiQueryConfig<any, Record<string, string>, Record<string, any>>, ReturnType extends QueryReturnType<T>, PathParamsType extends QueryPathParamsType<T>, UrlParamsType extends QueryUrlParamsType<T> >( apiQueryConfig: ApiQueryConfig<ReturnType, PathParamsType, UrlParamsType>, pathParams?: PathParamsType, urlParams?: UrlParamsType, axiosRequestConfig?: AxiosRequestConfig, tanstackConfig?: UseQueryOptions< AxiosResponse<ReturnType>, Error, AxiosResponse<ReturnType>, QueryKey > ): UseQueryResult<AxiosResponse<ReturnType, any>, Error> => { const apiClient = useApiClient(); const [queryFn, setQueryFn] = useState< QueryFunction<AxiosResponse<ReturnType, any>> | undefined >(undefined); const axiosConfigNonOverridable = { params: urlParams || {}, }; const axiosConfigOverridable: AxiosRequestConfig = { timeout: 10 * 1000, }; const mergedAxiosRequestConfig: AxiosRequestConfig = { ...axiosConfigOverridable, ...(axiosRequestConfig || {}), ...axiosConfigNonOverridable, }; const tanstackConfigNonOverridable: typeof tanstackConfig = { enabled: !!apiClient && (tanstackConfig?.enabled || true), }; const tanstackConfigOverridable: typeof tanstackConfig = { networkMode: "online", retry: 2, retryOnMount: true, staleTime: Infinity, cacheTime: 10 * 60 * 1000, refetchOnMount: true, refetchOnWindowFocus: false, refetchOnReconnect: true, }; const mergedTanstackConfig: typeof tanstackConfig = { ...tanstackConfigOverridable, ...(tanstackConfig || {}), ...tanstackConfigNonOverridable, }; const path = pathParams ? Object.entries(pathParams).reduce( (accPath, [key, value]) => accPath.replace(`{${key}}`, value), apiQueryConfig.apiPath ) : apiQueryConfig.apiPath; const queryKey = combineQueryKey( apiQueryConfig.queryKey.baseQueryKey, { ...pathParams, ...urlParams }, apiQueryConfig.queryKey.dynamicQueryKey ); useEffect(() => { if (apiClient) { console.log(apiClient); setQueryFn(() => apiClient!.get(path, mergedAxiosRequestConfig)); } // We should not use exhaustive deps here. Deps should be intentional. // eslint-disable-next-line react-hooks/exhaustive-deps }, [apiClient]); if (!queryFn) { return { isLoading: true } as UseQueryResult< AxiosResponse<ReturnType, any>, Error >; } return useQuery<AxiosResponse<ReturnType>, Error>( queryKey, queryFn!, mergedTanstackConfig ); }; export { useApiQuery };
解决方案
核心问题是通过useState和useEffect延迟构建queryFn,导致不得不条件返回,违反了Hook必须在每次渲染中按顺序调用的规则。解决思路是直接在useQuery内部定义queryFn,利用React Query的enabled选项控制查询时机,无需额外状态管理。
修改后的完整代码
import { QueryFunction, QueryKey, UseQueryOptions, UseQueryResult, useQuery, } from "@tanstack/react-query"; import { AxiosRequestConfig, AxiosResponse } from "axios"; import { ApiQueryConfig, QueryPathParamsType, QueryReturnType, QueryUrlParamsType, useApiClient, } from "@api"; import { combineQueryKey } from "./utils"; const useApiQuery = < T extends ApiQueryConfig<any, Record<string, string>, Record<string, any>>, ReturnType extends QueryReturnType<T>, PathParamsType extends QueryPathParamsType<T>, UrlParamsType extends QueryUrlParamsType<T> >( apiQueryConfig: ApiQueryConfig<ReturnType, PathParamsType, UrlParamsType>, pathParams?: PathParamsType, urlParams?: UrlParamsType, axiosRequestConfig?: AxiosRequestConfig, tanstackConfig?: UseQueryOptions< AxiosResponse<ReturnType>, Error, AxiosResponse<ReturnType>, QueryKey > ): UseQueryResult<AxiosResponse<ReturnType, any>, Error> => { const apiClient = useApiClient(); // 合并axios配置 const mergedAxiosRequestConfig: AxiosRequestConfig = { timeout: 10 * 1000, // 默认可覆盖配置 ...(axiosRequestConfig || {}), params: urlParams || {}, // 不可覆盖的参数配置 }; // 合并tanstack query配置 const mergedTanstackConfig: UseQueryOptions< AxiosResponse<ReturnType>, Error, AxiosResponse<ReturnType>, QueryKey > = { networkMode: "online", retry: 2, retryOnMount: true, staleTime: Infinity, cacheTime: 10 * 60 * 1000, refetchOnMount: true, refetchOnWindowFocus: false, refetchOnReconnect: true, // 默认可覆盖配置 ...(tanstackConfig || {}), enabled: !!apiClient && (tanstackConfig?.enabled ?? true), // 核心:只有apiClient就绪时才启用查询 }; // 处理路径参数 const path = pathParams ? Object.entries(pathParams).reduce( (accPath, [key, value]) => accPath.replace(`{${key}}`, value), apiQueryConfig.apiPath ) : apiQueryConfig.apiPath; // 生成查询key const queryKey = combineQueryKey( apiQueryConfig.queryKey.baseQueryKey, { ...pathParams, ...urlParams }, apiQueryConfig.queryKey.dynamicQueryKey ); // 直接在useQuery中定义queryFn,无需状态管理 const queryFn: QueryFunction<AxiosResponse<ReturnType>> = () => { if (!apiClient) { throw new Error("API Client not initialized"); } return apiClient.get(path, mergedAxiosRequestConfig); }; // 每次渲染都调用useQuery,符合Hook规则 return useQuery<AxiosResponse<ReturnType>, Error>( queryKey, queryFn, mergedTanstackConfig ); }; export { useApiQuery };
关键改动说明
- 移除状态管理逻辑:删掉了
useState和useEffect对queryFn的延迟初始化,直接在useQuery内部定义queryFn,避免条件返回导致的Hook规则违反。 - 优化enabled控制:在合并后的tanstack配置中,
enabled选项确保只有apiClient就绪且用户配置允许时,才执行查询,替代原有的延迟构建逻辑。 - 简化配置合并:调整axios和tanstack配置的合并顺序,确保默认可覆盖配置、用户自定义配置、不可覆盖配置的优先级正确。
- 符合Hook规则:每次渲染都会调用
useQuery,没有条件分支跳过Hook调用,完全遵守React Hook的使用规则。
内容的提问来源于stack exchange,提问作者steventnorris
相关产品推荐
相关产品推荐

