英文:
Conditional Use of a React Hook
问题
我正在尝试包装 useQuery 来集中化我的使用。但是,我遇到的问题是 queryFn 在运行时构建,因此在包装在我的自定义钩子中时,我必须有条件地返回钩子的承诺,这取决于 queryFn 是否准备好。这违反了钩子的规则。是否有人有关于如何正确包装 useQuery 在我的自定义钩子中的信息?当前状态下的代码如下。要查看的主要部分是返回和如何设置 queryFn。这是问题的关键。
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));
}
// 我们不应该在这里使用详尽的依赖项。依赖项应该是有意的。
// 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 };
希望这可以帮助您正确包装 useQuery 在您的自定义钩子中。
英文:
I am attempting to wrap useQuery to centralize my use of it. However, the issue I am running into is the queryFn is built at runtime, so when wrapped in my custom hook, I have to conditionally return the hook's promise based on whether the queryFn is ready or not. This breaks the rules of hooks. Does anyone have information on how to properly wrap the useQuery in my custom hook? Code in it's current state is below. The main bit to look at is the return and how queryFn is being set. That's the crux of the issue.
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 };
答案1
得分: 1
我同意@kca的评论,你应该考虑使用多个较小的钩子。而要返回“nothing”的方法是通过设置enable: true
。
我提出了一些建议性的更改。最重要的是,我用常量替换了queryFn
状态,并在queryFn === undefined
时设置enabled: false
。
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 axiosConfigNonOverridable = {
params: urlParams || {},
};
const axiosConfigOverridable: AxiosRequestConfig = {
timeout: 10 * 1000,
};
const mergedAxiosRequestConfig: AxiosRequestConfig = {
...axiosConfigOverridable,
...(axiosRequestConfig || {}),
...axiosConfigNonOverridable,
};
// 将 `path` 和 `queryFn` 移到这里,以便在收集 tanstack 配置之前知道 `queryFn` 的状态
const path = pathParams
? Object.entries(pathParams).reduce(
(accPath, [key, value]) => accPath.replace(`{${key}}`, value),
apiQueryConfig.apiPath
)
: apiQueryConfig.apiPath;
// `queryFn` 直接依赖于 `apiClient`、`path` 和 `mergedAxiosRequestChanges`,因此不应该是一个状态
const queryFn = apiClient
? () => apiClient.get(path, mergedAxiosRequestConfig)
: undefined;
const tanstackConfigNonOverridable: typeof tanstackConfig = {
enabled:
!!apiClient &&
// 如果没有 `queryFn`,则禁用查询
!!queryFn &&
// 检查是否为确切的 false,以前的实现
// 每次都会将 `false` 替换为 `true`
(tanstackConfig?.enabled !== false),
};
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 queryKey = combineQueryKey(
apiQueryConfig.queryKey.baseQueryKey,
{ ...pathParams, ...urlParams },
apiQueryConfig.queryKey.dynamicQueryKey
);
const res = useQuery<AxiosResponse<ReturnType>, Error>(
queryKey,
// 与非空断言 (`!`) 不同,如果我们意外地处于该禁止状态,最好返回
// 明确的错误
queryFn || () => Promise.reject(new Error("Missing query function")),
mergedTanstackConfig
);
return {
...res,
// 在 `apiClient` 加载时将 `isLoading` 覆盖为 `true`
isLoading: res.isLoading || !apiClient
};
英文:
I agree with @kca's comment, you should consider using multiple smaller hooks. And the way to return "nothing" is by setting enable: true
.
I've made some suggestion changes. Most notably I've replaced the queryFn
state with a constant and set enabled: false
if queryFn === undefined
.
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 axiosConfigNonOverridable = {
params: urlParams || {},
};
const axiosConfigOverridable: AxiosRequestConfig = {
timeout: 10 * 1000,
};
const mergedAxiosRequestConfig: AxiosRequestConfig = {
...axiosConfigOverridable,
...(axiosRequestConfig || {}),
...axiosConfigNonOverridable,
};
// move `path` and `queryFn` here so we know the status
// of `queryFn` before gathering the tanstack config
const path = pathParams
? Object.entries(pathParams).reduce(
(accPath, [key, value]) => accPath.replace(`{${key}}`, value),
apiQueryConfig.apiPath
)
: apiQueryConfig.apiPath;
// `queryFn` is directly dependent on `apiClient`, `path` and
// `mergedAxiosRequestChanges`, therefore it shouldn't be a state
const queryFn = apiClient
? () => apiClient.get(path, mergedAxiosRequestConfig)
: undefined;
const tanstackConfigNonOverridable: typeof tanstackConfig = {
enabled:
!!apiClient &&
// disable the query if we do not have the `queryFn`
!!queryFn &&
// check for exactly false, the previous implementation
// would have `false` replaced with `true` every time
(tanstackConfig?.enabled !== false),
};
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 queryKey = combineQueryKey(
apiQueryConfig.queryKey.baseQueryKey,
{ ...pathParams, ...urlParams },
apiQueryConfig.queryKey.dynamicQueryKey
);
const res = useQuery<AxiosResponse<ReturnType>, Error>(
queryKey,
// instead of non-null assertion (`!`), we might be better of returning
// an explicit error if we accidentally end up in that forbidden state
queryFn || () => Promise.reject(new Error("Missing query function")),
mergedTanstackConfig
);
return {
...res,
// overwrite `isLoading` with `true` while `apiClient` is loading
isLoading: res.isLoading || !apiClient
};
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论