为什么我不推荐你用 React Query

为什么我不推荐你用 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);
}

有了这 30 根毫毛,我们就可以开始七十二变了。

解决问题

假设我们有一个 fetchUserInfo 的请求函数,我们先用上面的 useInjectable 给它“赋能”一下:

const $fetchUserInfo = useInjectable(fetchUserInfo);

然后我们看看如何通过它把一个个简单的 hook 函数串联起来。

  • 首先,也是请求库最基础的功能就是把请求结果记录在 react 的 state 里:
export function useResult(injectableFn, init) {
  const [result, setResult] = useState(init);

  useInject(injectableFn, (f) => ((...args) => f(...args).then(thruSet(setResult))));
  return result;
}

用起来也很简单,它就比 useState 多一个参数,就是上面的 $fetchUserInfo

const userInfo = useResult($fetchUserInfo);
  • 同理,我们也可以记录请求状态
const isLoading = useLoading($fetchUserInfo);
  • 记录错误
const error = useError($fetchUserInfo);
  • 错误重试
useRetry($fetchUserInfo);
  • 自动执行
useRun($fetchUserInfo, [userId]);
  • 使用缓存
useCache($fetchUserInfo, options);
  • ...

我想你已经明白我的意思,不明白请薅自己头发。 完整的代码在这里.

总结

瑞士军刀是一种看起来很酷,但用起来未必顺手的工具。类似的程序设计模式是不被主流所推崇的, unix 那种简单可组合的模式才是程序设计的最佳典范。

posted on 2023-03-03 12:18  漫思  阅读(791)  评论(0编辑  收藏  举报

导航