场景:在 React 中封装不同的 hook,难免 hooks 中有些逻辑相同,有些部分不同,但是 React 中会有个限制,就是 hook 必须在顶端执行,不能用条件语句去判断。具体来说,我有这样一个需求,我将无限滚动加载的逻辑封装在一个 hook 里面,但是每个用到无限滚动加载的页面,仅是接口请求不一样、缓存的 key 不一样, 我封装了多个 hook,而无限滚动加载的逻辑封装在一个 hook 里面
代码展现:
这是一个文章列表的无限下拉加载展示代码,对于其他页面的下拉加载,只有 useInfiniteQuery 的 queryKey 和 queryFn 不一样
// useArticleList.ts
import { useEffect } from "react";
import { useInView } from "react-intersection-observer";
import { PostPaginated, PostParams } from "@/types/article";
import { useInfiniteQuery } from "@tanstack/react-query";
import { articleKeys } from "./request/articleKeys";
import { articleService } from "@/services/article";
type ArticleListType = "all" | "category" | "tag" | "date";
interface UseArticleListOptions {
type: ArticleListType;
params?: PostParams;
}
export function useArticleList({
params = {},
}: UseArticleListOptions) {
const { ref, inView } = useInView();
const {
data,
isLoading,
error,
hasNextPage,
fetchNextPage,
isFetchingNextPage
} = useInfiniteQuery<PostPaginated>({
queryKey: articleKeys.lists(),
queryFn: async ({ pageParam }) => {
const response = await articleService.getArticles({
...params,
cursor: pageParam as string | undefined,
limit: 10,
});
return response.data;
},
initialPageParam: undefined,
getNextPageParam: (lastPage) => lastPage.nextCursor,
});
useEffect(() => {
if (inView && hasNextPage) {
fetchNextPage();
}
}, [inView, hasNextPage, fetchNextPage]);
const articles = data?.pages.flatMap((page) => page.content) ?? [];
return {
articles,
isLoading,
error,
loadMoreProps: {
hasNextPage,
isFetchingNextPage,
loadMoreRef: ref,
},
};
}
最好的办法是 queryFn 和 queryKey 接受传入, 当然我这里是要体现 同一个 hook 中执行不同的 hook, 所以抽离了多个 hook
// article.ts
export function useInfiniteArticles(options: PostParams = {}) {
return useInfiniteQuery<PostPaginated>({
queryKey: articleKeys.lists(),
queryFn: async ({ pageParam }) => {
const response = await articleService.getArticles({
...options,
cursor: pageParam as string | undefined,
limit: 10,
});
return response.data;
},
initialPageParam: undefined,
getNextPageParam: (lastPage) => lastPage.nextCursor,
});
}
export function useInfiniteArticlesByCategory(options: PostParams = {}) {
return useInfiniteQuery<PostPaginated>({
queryKey: articleKeys.category(options.categoryId!),
queryFn: async ({ pageParam }) => {
const response = await articleService.getArticlesByCategoryId(
options.categoryId!,
{
cursor: pageParam as string | undefined,
limit: 10,
},
);
return response.data;
},
initialPageParam: undefined,
getNextPageParam: (lastPage) => lastPage.nextCursor,
});
}
然后在 useArticle.ts 根据条件的不同执行不同的 hook
import { useEffect } from "react";
import { useInView } from "react-intersection-observer";
import {
useInfiniteArticles,
useInfiniteArticlesByCategory,
} from "./request/article";
import { PostParams } from "@/types/article";
type ArticleListType = "all" | "category" | "tag" | "date";
interface UseArticleListOptions {
type: ArticleListType;
params?: PostParams;
}
export function useArticleList({
type = "all",
params = {},
}: UseArticleListOptions) {
const { ref, inView } = useInView();
// 关键代码在此!!!
const queryHook = (() => {
switch (type) {
case "category":
// eslint-disable-next-line react-hooks/rules-of-hooks
return useInfiniteArticlesByCategory(params);
default:
// eslint-disable-next-line react-hooks/rules-of-hooks
return useInfiniteArticles(params);
}
})();
const {
data,
isLoading,
error,
hasNextPage,
fetchNextPage,
isFetchingNextPage,
} = queryHook;
useEffect(() => {
if (inView && hasNextPage) {
fetchNextPage();
}
}, [inView, hasNextPage, fetchNextPage]);
const articles = data?.pages.flatMap((page) => page.content) ?? [];
return {
articles,
isLoading,
error,
loadMoreProps: {
hasNextPage,
isFetchingNextPage,
loadMoreRef: ref,
},
};
}
最主要的代码是 queryhook 这个立即执行方法, 在这个方法里面可以根据不同的条件执行不同的 hook
const queryHook = (() => {
switch (type) {
case "category":
// eslint-disable-next-line react-hooks/rules-of-hooks
return useInfiniteArticlesByCategory(params);
case "tag":
// eslint-disable-next-line react-hooks/rules-of-hooks
return useInfiniteArticlesByTag(params);
case "date":
// eslint-disable-next-line react-hooks/rules-of-hooks
return useInfiniteArticlesByDate(params);
default:
// eslint-disable-next-line react-hooks/rules-of-hooks
return useInfiniteArticles(params);
}
})();
刚刚说到仅仅是 querykey 和 queryFn 不同,没有必要再次封装 hooks,参数传入就行
// useArticleList.ts
export function useArticleList({
queryKey,
queryFn
}: UseArticleListOptions) {
const { ref, inView } = useInView();
const {
data,
isLoading,
error,
hasNextPage,
fetchNextPage,
isFetchingNextPage,
} = useInfiniteQuery<PostPaginated>({
queryKey: [queryKey],
queryFn: async ({ pageParam }) => {
const response = await queryFn({
cursor: pageParam as string | undefined,
limit: 10,
});
return response;
},
initialPageParam: undefined,
getNextPageParam: (lastPage) => lastPage.nextCursor,
});;
使用
const { articles, isLoading, error, loadMoreProps } = useArticleList({
queryKey: "articles",
queryFn: (params) =>
articleService.getArticles(params).then((res) => res.data),
});