为什么我不推荐你用 React Query

为什么我不推荐你用 React Query
不知
前言
React Query(现已更名为 TanStack Query 并支持多种框架) 是一个优秀的前端请求库,被许多团队采用,我们团队内部使用也有两年左右了。
然而,我对这种“瑞士军刀”式的库是非常排斥的,每次看到这个长长的函数签名,我就失去了看文档的兴趣:
const {
data,
dataUpdatedAt,
error,
errorUpdatedAt,
failureCount,
failureReason,
isError,
isFetched,
isFetchedAfterMount,
isFetching,
isPaused,
isLoading,
isLoadingError,
isPlaceholderData,
isPreviousData,
isRefetchError,
isRefetching,
isStale,
isSuccess,
refetch,
remove,
status,
fetchStatus,
} = useQuery({
queryKey,
queryFn,
cacheTime,
enabled,
networkMode,
initialData,
initialDataUpdatedAt,
keepPreviousData,
meta,
notifyOnChangeProps,
onError,
onSettled,
onSuccess,
placeholderData,
queryKeyHashFn,
refetchInterval,
refetchIntervalInBackground,
refetchOnMount,
refetchOnReconnect,
refetchOnWindowFocus,
retry,
retryOnMount,
retryDelay,
select,
staleTime,
structuralSharing,
suspense,
useErrorBoundary,
})
今天我终于决定写一篇文章谈一谈这个问题,并给出我的解决方案。
瑞士军刀式函数
为了解决复杂的问题,我们的工具也会变得多样,瑞士军刀就是一个典型。
在程序设计上,通过复杂的参数、选项来控制同一个函数应对多种场景,也可已称作一种【瑞士军刀】模式。
这种模式往往更容易被采用,来一种场景就加参数嘛,根本不必多想。 这种简单粗暴的模式虽然在设计上容易,但是带来的问题也很明显:
- 首先是参数项多了就会导致接口很复杂,调用方很难轻易掌握参数之间的关系,以及怎么配合来满足自己的场景。
- 其次,程序内部的逻辑容易变得复杂臃肿,难以维护。
- 不利于外部扩展,第三方很难用自己的实现替换内部的逻辑。
- 对于前端来讲,也不利于前端打包时进行 Tree-Shaking,即使这个依赖里大多数逻辑你都用不到,你也要把它打进包里。
React Query 在一个函数里封装了请求状态、错误处理、缓存、自动拉取等太多能力,但是可能 好多我都用不到,或者我期望用别的方式来解决,这都是不可以的,必须全盘接收它为你设定好的【最佳实践】,才能用的舒服。
比如,我希望把错误抛到容器层处理就很难做到,因为它已经把错误收到状态里了。
以上是我不推荐你用 React Query 的理由。虽然这些想法早已经想写出来,但是只是去批评没有太大意义,必须要有一个更好的设计。直到最近,我才搞出一个我认为相对理想的设计方案。
可组合的简单函数
我期望的方案是有一组简单函数可以按需调配,满足各种场景。就像一个工具箱,里面的每一件工具都可以用来做一件事,它们可以协作,也可以被替换,也可以任意补充。
为了实现这一目的,我只有不停地薅头发,最后我终于薅出了 30 行代码:
import {MutableRefObject, useCallback, useRef} from 'react';
const map = new WeakMap();
type Func = (...args: any[]) => any;
type Wrapper<F extends Func> = (f: F, callContext: any) => F;
export function useInjectable<F extends Func>(fn: F): F {
const ref = useRef<[F, Wrapper<F>[], any]>();
ref.current = [fn, [], {}];
const f = useCallback((...args: Parameters<F>) => {
const [func, injects] = ref.current!;
const callContext = {};
return injects.reduce((i, w) => w(i, callContext), func)(...args);
}, []) as F;
map.set(f, ref);
return f;
}
export function getInjectContext<F extends Func>(fn: F) {
const ref = map.get(fn) as MutableRefObject<[F, Wrapper<F>[], any]>;
return ref.current[2];
}
export function useInject<F extends Func>(fn: F, wrapper: Wrapper<F>) {
const ref = map.get(fn) as MutableRefObject<[F, Wrapper<F>[]]>;
ref.current[1].push(wrapper);