動態

詳情 返回 返回

@tanstack/react-query 實踐 - 動態 詳情

@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返回的數據就會被垃圾回收機制直接回收。

11. 組件來回切換狀態,useQuery一直被該組件使用,而不會清除掉緩存裏的數據怎麼辦?

如下圖所示,點擊麪包屑,table根據不同的id,獲取不同的list

image.png

export const useFetchDepartmentMemberList = () => {
  const { data: info } = useFetchInfo(false);
  const [id, setId] = useState<string>('');

  const { data, isFetching: loading } = useQuery<IMember[]>({
    queryKey: [ApiAction.ListDepartmentMember, id],
    initialData: [],
    gcTime: 0,
    enabled: !!id, // id不存在不會發請求
    queryFn: async () => {
      const { data } = await listDepartmentMember(info.id, id);

      return data?.data ?? [];
    },
  });

  return { data, loading, setId };
};


  const {
    data: departmentMemberList,
    loading: departmentMemberListLoading,
    setId,
  } = useFetchDepartmentMemberList();

  useEffect(() => {
    setId(departmentParentId ? departmentParentId : ''); // 將id置為"",這樣緩存裏的數據就會被清理掉
  }, [departmentParentId, setId]);

12. useQuery 設置會影響 isSuccess

export const useFetchTenantInfo = (
  showEmptyModelWarn = false,
): ResponseGetType<ITenantInfo | undefined> => {
  const { t } = useTranslation();
  const results = useQueries({
    queries: [
      {
        queryKey: ['tenantInfo'],
        // initialData: {},
        placeholderData: {},
        // gcTime: 0,
        queryFn: async () => {
          const { data: res } = await userService.get_tenant_info();
          if (res.code === 0) {
            // llm_id is chat_id
            // asr_id is speech2txt
            const { data } = res;

            data.chat_id = data.llm_id;
            data.speech2text_id = data.asr_id;

            return data;
          }

          return res;
        },
      },
      {
        queryKey: ['FetchEnableAdmin'],
        queryFn: fetchEnableAdminQueryFn,
      },
      {
        queryKey: ['FetchIsAdmin'],
        queryFn: fetchIsAdminQueryFn,
      },
    ],
  });

  const [data, enableAdmin, isAdmin] = results;

  const tenantInfo = data.data;

  const allCompleted = results.every((r) => !r.isPending);
  const allSuccess = results.every((r) => r.isSuccess);
  console.log('🚀 ~ useFetchTenantInfo ~ allCompleted:', allCompleted);
  console.log('🚀 ~ useFetchTenantInfo ~ allSuccess:', allSuccess);
  console.log(
    '🚀 ~ useFetchTenantInfo ~ results:',
    results.map((x) => x.data),
  );

  useEffect(() => {
    if (allCompleted && allSuccess) {
      const hideWarn = enableAdmin.data && !isAdmin.data;
      if (
        showEmptyModelWarn &&
        (isEmpty(tenantInfo.embd_id) || isEmpty(tenantInfo.llm_id)) &&
        !hideWarn
      ) {
        Modal.destroyAll(); // Not elegant
        Modal.warning({
          title: t('common.warn'),
          content: (
            <div
              dangerouslySetInnerHTML={{
                __html: DOMPurify.sanitize(t('setting.modelProvidersWarn')),
              }}
            ></div>
          ),
          onOk() {
            history.push('/user-setting/model');
          },
        });
      }
    }
  }, [
    allCompleted,
    allSuccess,
    enableAdmin.data,
    isAdmin.data,
    showEmptyModelWarn,
    t,
    tenantInfo,
    tenantInfo.embd_id,
    tenantInfo.llm_id,
  ]);

  return { data: data.data, loading: data.isPending };
};

8282fa5d904e6596089706ae57464b2d.png
上圖所示是設置initialData或者placeholderData導致isSuccess在還沒拿到後端數據前已經為true,如果我去掉這兩個屬性就不會有這個問題,如下圖所示
bde1fbc3e5b85e3e548375afd80ec648.png
上圖的行為就和我們期望的一致,但是大模型給出的答案和我測試不一樣。
@tanstack/react-query 5.51.8 initialData 或者 placeholderData 會影響 isPending或者isSuccess嗎

怎麼兼顧 isSuccess 在接口執行完畢為ture 而不是一開始就為true和 接口沒返回數據前有個初始值的問題?

  const allTrulySuccessful = results.every(query => query.isSuccess && !query.isPlaceholderData);

react query 的作者在How distinguish between initialData and fetched data?做了關於二者的區分説明,Placeholder and Initial Data in React Query 這裏也有解釋,尤其在請求接口失敗的情況下,因為initialData會緩存起來,即使接口失敗也會返回initialData的數據,而接口失敗,使用placeholderData,則useQuery會返回undefined。

user avatar aser1989 頭像 justbecoder 頭像 liushuigs 頭像 faurewu 頭像 xiangjiaochihuanggua 頭像 chongdongdedaxiongmao_kxfei 頭像 ruochuan12 頭像
點贊 7 用戶, 點贊了這篇動態!
點贊

Add a new 評論

Some HTML is okay.