@tanstack/react-query@5.35.5

1. isPending isLoading isFetching 傻傻分不清

  const { data: knowledgeList, isFetching: loading } = useQuery({
    queryKey: ['knowledgeList'],
    initialData: [],
    gcTime: 0,
  });

useQuery的isFetching是接口在请求中

React Query 5: When to use isLoading, isFetching, and isRefetching

2. 单独获取一个接口的loading状态

useQuery()会返回isFetching,但是往往component是分开写的,就是发起请求在一个component,而Spin在另一个component,这时候就需要独立的拿到isFetching

import {
  useIsFetching,
} from '@tanstack/react-query';

export const useKnowledgeDetailIsFetching = () => {
  return useIsFetching({ queryKey: ['knowledgeDetail'] }) > 0;
};

注意:useIsFetching()返回的是数字

Background Fetching Indicators

同理,对于useMutation

export const useChunkIsTesting = () => {
  return useIsMutating({ mutationKey: ['testChunk'] }) > 0;
};

3. queryClient.getQueryData与useQuery获取共享数据的区别

比如页面加载的时候使用useQuery请求到了数据,被@tanstack/react-query缓存了起来,在其他组件里想拿到该数据,通常会直接调用useQuery获取数据,但是在项目里出了问题,如下图,我在两个节点拖拽无法建立连线,因为线跟后端返回的数据是管理的,边节点里面调用了useQuery,每次有新线连接就会调用useQuery,这样导致我客户端的数据被接口返回的数据所覆盖,从而连接不成功。根本原因在于retryOnMount参数为true,在每次挂载组件时自动触发重新获取。
image.png
queryClient.getQueryData就不会在拖线的时候发送请求了

  const queryClient = useQueryClient();
  const flowDetail = queryClient.getQueryData<IFlow>(['flowDetail']);

4. 在不同组件共享useMutation获取的数据

通常都是用useQuery去获取代get方法的接口的数据的,但是有时候后端给的接口是post,需要提交表单数据,这个时候需要用button触发接口的调用,如果用useQuery的话,需要使用enabled:false禁用useQuery的默认加载调用的行为,然后结合refetch函数去手动调用,但是refetch不能传递参数,需要将参数传到state或者redux、zustand等状态管理库托管,所以还是用useMutation方便点,但是怎么在不同组件共享useMutation获取的数据?

export const useSelectTestingResult = (): ITestingResult => {
  const data = useMutationState({
    filters: { mutationKey: ['testChunk'] },
    select: (mutation) => {
      return mutation.state.data;
    },
  });
  return (data.at(-1) ?? { // 获取接口返回的最新的一条数据
    chunks: [],
    documents: [],
    total: 0,
  }) as ITestingResult;
};

参考:
Why useMutation is designed as not able to share status and data between multiple component? #6118

5. 模糊匹配useQuery缓存的数据

列表页面往往有很多查询条件,比如分页,搜索,排序等,@tanstack/react-query@5.35.5推荐将查询条件写进queryKey作为依赖,从而触发接口的重新请求,但是我们在不同的组件希望拿到被@tanstack/react-query@5.35.5缓存的数组,而不是层层传递,useQuery代码如下,
image.png
如果在不同的组件里使用useFetchNextChunkList,如果有组件mount,则useFetchNextChunkList会被多次执行,会导致每次的查询条件都是初始值,因为useState会被重新执行,所以只好选择 getQueriesData, Share state between components #2310 这种方式行不通

export const useSelectChunkList = () => {
  const queryClient = useQueryClient();
  const data = queryClient.getQueriesData<{
    data: IChunk[];
    total: number;
    documentInfo: IKnowledgeFile;
  }>({ queryKey: ['fetchChunkList'] });

  return data[0][1];
};

6. 缓存全局数据

Using react-query to store global state? #2852

7. 在发出请求到拿到数据中间会返回初始值initialData

我想保留之前的查询结果,会导致组件无效的rerender,即使用了placeholderData: keepPreviousData也不行。有待讨论

f20d206dd4bb04a1bdee6861d0aca6b.png

8. 自定义staleTime: 20 * 1000,设置initialData跟不设置该值有不同的表现

阅读了 React Query as a State Manager 自己做了如下测试
demo.tsx

import { useFetchFlowTemplates } from '@/hooks/flow-hooks';

const Inner = () => {
  const ret = useFetchFlowTemplates();
  const data = ret?.data;
  return <ul>{data?.map((x) => <li key={x.id}>{x.title}</li>)}</ul>;
};

const Demo = () => {
  const ret = useFetchFlowTemplates();
  const data = ret?.data;

  return (
    <section>
      <h6>{data?.length}</h6>
      {data && <Inner></Inner>}
    </section>
  );
};

export default Demo;

hooks

export const useFetchFlowTemplates = (): ResponseType<IFlowTemplate[]> => {
  const { data } = useQuery({
    queryKey: ['fetchFlowTemplates'],
    staleTime: 20 * 1000,
    initialData: [],
    queryFn: async () => {
      const { data } = await flowService.listTemplates();
      return data;
    },
  });

  return data;
};

上述代码不会发送请求,将initialData注释掉,如下,可以正常发送一条请求,有待进一步探究

export const useFetchFlowTemplates = (): ResponseType<IFlowTemplate[]> => {
  const { data } = useQuery({
    queryKey: ['fetchFlowTemplates'],
    staleTime: 20 * 1000,
    // initialData: [],
    queryFn: async () => {
      const { data } = await flowService.listTemplates();
      return data;
    },
  });

  return data;
};

9. useQuery分页查询,会重置之前请求到的数据

上代码:

export const useFetchNextChunkList = (): ResponseGetType<{
  data: IChunk[];
  total: number;
  documentInfo: IKnowledgeFile;
}> &
  IChunkListResult => {
  const { pagination, setPagination } = useGetPaginationWithRouter();
  const { documentId } = useGetKnowledgeSearchParams();
  const { searchString, handleInputChange } = useHandleSearchChange();
  const [available, setAvailable] = useState<number | undefined>();
  const debouncedSearchString = useDebounce(searchString, { wait: 500 });

  const { data, isFetching: loading } = useQuery({
    queryKey: [
      'fetchChunkList',
      documentId,
      pagination.current,
      pagination.pageSize,
      debouncedSearchString,
      available,
    ],

    initialData: { data: [], total: 0, documentInfo: {} },
    // placeholderData: keepPreviousData,
    gcTime: 0,
    queryFn: async () => {
      const { data } = await kbService.chunk_list({
        doc_id: documentId,
        page: pagination.current,
        size: pagination.pageSize,
        available_int: available,
        keywords: searchString,
      });
      if (data.code === 0) {
        const res = data.data;
        return {
          data: res.chunks,
          total: res.total,
          documentInfo: res.doc,
        };
      }

      return (
        data?.data ?? {
          data: [],
          total: 0,
          documentInfo: {},
        }
      );
    },
  });

  const onInputChange: React.ChangeEventHandler<HTMLInputElement> = useCallback(
    (e) => {
      setPagination({ page: 1 });
      handleInputChange(e);
    },
    [handleInputChange, setPagination],
  );

  const handleSetAvailable = useCallback(
    (a: number | undefined) => {
      setPagination({ page: 1 });
      setAvailable(a);
    },
    [setAvailable, setPagination],
  );

  return {
    data,
    loading,
    pagination,
    setPagination,
    searchString,
    handleInputChange: onInputChange,
    available,
    handleSetAvailable,
  };
};

每次在切换页码的时候useQuery之前返回的数据都会被重置为initialData,从而导致依赖该数据的页面其他部分在切换页面的时候被重新渲染。但是按照官网的示例做加上参数 placeholderData: keepPreviousData, 并不能完全解决这个问题Paginated / Lagged Queries

解决办法:
删除initialData ,加上

placeholderData: (previousData) => previousData ?? []

Conflict Between initialData and placeholderData – Either undefined or UI Flicker​ #8183

10. 关于 gcTime

需求:编辑跟新增为同一个modal,编辑的时候调用接口并显示数据,新增的时候不调用接口切且不显示数据,如下图所示
image.png

代码如下,
ChunkCreatingModal.tsx

  const { data } = useFetchChunk(chunkId);

  useEffect(() => { // modal显示,从接口获取数据,将数据赋值给form
    if (data?.code === 0) {
      const { content_with_weight, available_int } = data.data;
      form.setFieldsValue({ ...data.data, content: content_with_weight });
    }
  }, [data, form, chunkId]);

hooks.ts

export const useFetchChunk = (chunkId?: string): ResponseType<any> => {
  const { data } = useQuery({
    queryKey: ['fetchChunk'],
    enabled: !!chunkId, // 只有编辑的时候才调用接口函数
    initialData: {},
    queryFn: async () => {
      const data = await kbService.get_chunk({
        chunk_id: chunkId,
      });

      return data;
    },
  });

  return data;
};

上面的代码只能保证新增的时候,不调用接口,但是点击新增按钮,弹窗依然会显示之前的数据,那怎么办?

官网的两句话

Query results that have no more active instances of useQuery, useInfiniteQuery or query observers are labeled as "inactive" and remain in the cache in case they are used again at a later time.

By default, "inactive" queries are garbage collected after 5 minutes.

To change this, you can alter the default gcTime for queries to something other than 1000 * 60 * 5 milliseconds.

翻译过来是:

如果查询结果没有更多的 useQuery、 useInfiniteQuery 或查询观察者的活动实例,那么它们将被标记为 “非活动”,并保留在缓存中,以防日后再次使用。

默认情况下,“非活动” 查询在 5 分钟后被垃圾收集。

要改变这一点,您可以将查询的默认 gcTime 更改为 1000 * 60 * 5 毫秒以外的值。

结合tanstack query 开发者工具也可以看出:

image.png

当useQuery返回的数据被引用的时候,该query会被标位stale状态,如果我把modal关了,再看下该query的状态
image.png
这个时候query被标记为inactive状态,经过五分钟后,该query会从开发工具中消失,也就是被垃圾回收了。所以如果需要实现开头的功能只需要将gcTime设置为0,modal关闭,则该query失去了引用,那么query返回的数据就会被垃圾回收机制直接回收。


assassin_cike
1.3k 声望74 粉丝

生活不是得过且过


« 上一篇
react 继续踩坑
下一篇 »
SSE post 实践