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

如何合规封装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 };

关键改动说明

  1. 移除状态管理逻辑:删掉了useState和useEffect对queryFn的延迟初始化,直接在useQuery内部定义queryFn,避免条件返回导致的Hook规则违反。
  2. 优化enabled控制:在合并后的tanstack配置中,enabled选项确保只有apiClient就绪且用户配置允许时,才执行查询,替代原有的延迟构建逻辑。
  3. 简化配置合并:调整axios和tanstack配置的合并顺序,确保默认可覆盖配置、用户自定义配置、不可覆盖配置的优先级正确。
  4. 符合Hook规则:每次渲染都会调用useQuery,没有条件分支跳过Hook调用,完全遵守React Hook的使用规则。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.18 05:35:11