+åÔ深入淺出 TanStack Queryï¼ˆä¸‰ï¼‰ï¼šåœ¨å‘¼å« invalidateQueries 後發生了什麼事 | Alex Liu

深入淺出 TanStack Queryï¼ˆä¸‰ï¼‰ï¼šåœ¨å‘¼å« invalidateQueries 後發生了什麼事

• 8 min read

在使用 TanStack Query æ™‚ï¼Œæˆ‘å€‘ç¶“å¸¸éœ€è¦æ‰‹å‹•地讓æŸäº›æˆ–是特定的 query 釿–°ç™¼é€è«‹æ±‚來å–得最新的資料,此時,我們å¯ä»¥ä½¿ç”¨ invalidateQueries 方法。這篇文章將深入了解在調用 invalidateQueries 後 TanStack Query åšäº†ä»€éº¼äº‹ï¼Œä»¥åŠ invalidateQueries 與 refetch 的使用比較。

å‰è¨€

本篇的 TanStack Query 版本為 5.28.7

這是一個跟 TanStack Query 相關的深入原始碼系列文章,TanStack Query 的架構é¾å¤§ä¸”迭代快速,所以這個系列會ä¸å®šæœŸæ›´æ–°ï¼Œä¸‹åˆ—是目å‰å·²ç¶“發布的文章:

  1. 深入淺出 TanStack Queryï¼ˆä¸€ï¼‰ï¼šåœ¨å‘¼å« useQuery 後發生了什麼事
  2. 深入淺出 TanStack Queryï¼ˆäºŒï¼‰ï¼šåœ¨å‘¼å« useMutation 後發生了什麼事
  3. 深入淺出 TanStack Queryï¼ˆä¸‰ï¼‰ï¼šåœ¨å‘¼å« invalidateQueries 後發生了什麼事

invalidateQueries 是什麼?

下列是 TanStack Query å®˜æ–¹æ–‡ä»¶ä¸­å°æ–¼ invalidateQueries 的說明:

The invalidateQueries method can be used to invalidate and refetch single or multiple queries in the cache based on their query keys or any other functionally accessible property/state of the query. By default, all matching queries are immediately marked as invalid and active queries are refetched in the background.

根據文件我們å¯ä»¥ç°¡å–®ç†è§£ invalidateQueries 是 queryClient 上的一個方法,這個方法å¯ä»¥è®“我們ä¾ç…§éœ€æ±‚,手動的讓æŸäº›æˆ–是特定的 query 釿–°ç™¼é€è«‹æ±‚ã€‚åƒæ˜¯ä¸‹åˆ—兩種情境就很é©åˆä½¿ç”¨ invalidateQueries。

情境一:當使用 useMutation 新增一筆資料後,我們å¯èƒ½æœƒæƒ³è¦è®“ useQuery 釿–°å–å¾—æœ€æ–°çš„çµæžœï¼Œé€™æ™‚我們å¯ä»¥é€™æ¨£åšã€‚

const queryClient = useQueryClient()

const { data } = useQuery({
  queryKey: ['TODOS'],
  queryFn: fetchTodos
})

const { mutate } = useMutation({
  mutationFn: addTodo,
  onSuccess() {
    return queryClient.invalidateQueries({ queryKey: ['TODOS'] })
  }
})

情境二:畫é¢ä¸Šæœ‰ã€Œé‡æ–°è¼‰å…¥ã€æŒ‰éˆ•,當使用者點擊時我們需è¦å–得最新的資料,這時我們å¯ä»¥é€™æ¨£åšã€‚

<script setup lang="ts">
const queryClient = useQueryClient()

const { data } = useQuery({
  queryKey: ['TODOS'],
  queryFn: fetchTodos
})

const onRefetch = () => {
  return queryClient.invalidateQueries({ queryKey: ['TODOS'] })
}
</script>

<template>
  <button @click="onRefetch">釿–°è¼‰å…¥</button>
</template>

åœ¨å‘¼å« invalidateQueries 後發生了什麼事

為了一窺究竟,我們來看看 queryClient 上的 invalidateQueries 實作。

class QueryClient {
  invalidateQueries(
    filters: InvalidateQueryFilters = {},
    options: InvalidateOptions = {},
  ): Promise<void> {
    return notifyManager.batch(() => {
      this.#queryCache.findAll(filters).forEach((query) => {
        query.invalidate()
      })

      if (filters.refetchType === 'none') {
        return Promise.resolve()
      }
      const refetchFilters: RefetchQueryFilters = {
        ...filters,
        type: filters.refetchType ?? filters.type ?? 'active',
      }
      return this.refetchQueries(refetchFilters, options)
    })
  }

}

根據程å¼ç¢¼æˆ‘們知é“åœ¨å‘¼å« invalidateQueries 後,TanStack Query 需è¦åŸ·è¡Œä¸‹åˆ—兩件事情:

  1. ä¾ç…§å‚³å…¥çš„ filters 找出所有的 queryï¼Œä¸¦å‘¼å« query 上的 invalidate 方法。
  2. ç¢ºèª filters.refetchType 是å¦ç‚º none,如果是則表示ä¸éœ€è¦é‡æ–°ç™¼é€è«‹æ±‚。åä¹‹å‰‡å‘¼å« refetchQueries。

接著嘗試更近一步的拆解這兩個步驟。

Query 上的 invalidate åšäº†ä»€éº¼äº‹æƒ…

è¦äº†è§£ invalidate åšäº†ä»€éº¼æˆ‘們å¯ä»¥åˆ° Query 這個類別中找到他的實作。

class Query extends Removable {
  #cache: QueryCache
  #observers: Array<QueryObserver<any, any, any, any, any>>

  invalidate(): void {
    if (!this.state.isInvalidated) {
      this.#dispatch({ type: 'invalidate' })
    }
  }

  #dispatch(action: Action<TData, TError>): void {
    const reducer = (
      state: QueryState<TData, TError>,
    ): QueryState<TData, TError> => {
      switch (action.type) {
        // å…¶ä»–çœç•¥

        case 'invalidate':
          return { ...state, isInvalidated: true }
      }
    }

    this.state = reducer(this.state)

    notifyManager.batch(() => {
      this.#observers.forEach((observer) => {
        observer.onQueryUpdate()
      })

      this.#cache.notify({ query: this, type: 'updated', action })
    })
  }
}

雖然程å¼ç¢¼çœ‹èµ·ä¾†å¾ˆå¤šï¼Œä½†å…¶å¯¦åªåšäº†å…©ä»¶äº‹æƒ…:

  1. 將態上的 isInvalidated 設定為 true。
  2. 通知所有的 observer 跟 cache 這個 query 已經被更新。

åœ¨å‘¼å« refetchQueries 後發生了什麼事

接著我們來看看 queryClient 上的 refetchQueries 實作。

class QueryClient {
  refetchQueries(
    filters: RefetchQueryFilters = {},
    options?: RefetchOptions,
  ): Promise<void> {
    const fetchOptions = {
      ...options,
      cancelRefetch: options?.cancelRefetch ?? true,
    }
    const promises = notifyManager.batch(() =>
      this.#queryCache
        .findAll(filters)
        .filter((query) => !query.isDisabled())
        .map((query) => {
          let promise = query.fetch(undefined, fetchOptions)
          if (!fetchOptions.throwOnError) {
            promise = promise.catch(noop)
          }
          return query.state.fetchStatus === 'paused'
            ? Promise.resolve()
            : promise
        }),
    )

    return Promise.all(promises).then(noop)
  }
}

一步步說明這段程å¼ç¢¼åšäº†é‚£äº›äº‹æƒ…:

  1. ä¾ç…§å‚³å…¥çš„ filters 找出所有的 query(這裡找到的會跟 invalidateQueries 裡é¢çš„一樣)。
  2. éŽæ¿¾æŽ‰æ‰€æœ‰ isDisabled() 為 true çš„ query。
  3. 調用剩下的 query 上的 fetch æ–¹æ³•é‡æ–°ç™¼é€è«‹æ±‚。

isDisabled 是一個 query 上的方法,這個方法ä¾ç…§å…©ä»¶äº‹æƒ…來判斷是å¦ç‚º false:

class Query extends Removable {
  isActive(): boolean {
    return this.#observers.some(
      (observer) => observer.options.enabled !== false,
    )
  }

  isDisabled(): boolean {
    return this.getObserversCount() > 0 && !this.isActive()
  }
}

這裡的程å¼ç¢¼é‚輯有點繞,簡單列點說明:

  1. 如果 query 沒有被任何 observer 訂閱則為 false。
  2. 如果 query 上的任一個 observer çš„ enabled ä¸ç‚º false 則為 false。

ç¶“éŽä¸Šè¿°é‡é‡çš„判斷æ¢ä»¶

看完了這個段è½é—œæ–¼ refetchQueries 的實作分æžï¼Œçµåˆä¸Šä¸€æ®µçš„內容我們å¯ä»¥å®Œæ•´æ‹¼æ¹Šå‡ºåœ¨å‘¼å« invalidateQueries 後發生了什麼事情。

invalidateQueries vs refetch

在一開始的範例中,我們æåˆ°äº†å¦‚æžœè¦è®“ useQuery 釿–°å–å¾—çµæžœï¼Œæˆ‘們å¯ä»¥ä½¿ç”¨ invalidateQueries。但這似乎有點é•èƒŒç›´è¦ºï¼Œé‡æ–°å–得(refetch)應該比無效(invalidateï¼‰æ›´ç›´è¦ºæ‰æ˜¯ï¼Œä¸¦ä¸” useQuery 也有 refetch 方法å¯ä»¥ä½¿ç”¨ï¼Œç‚ºä»€éº¼éœ€è¦ç‰¹åœ°å°å…¥ queryClient çš„ invalidateQueries 呢?

當然,上é¢çš„例å­å®Œå…¨å¯ä»¥æ”¹ä½¿ç”¨ refetch 方法。

const { data, refetch } = useQuery({
  queryKey: ['TODOS'],
  queryFn: fetchTodos
})

const { mutate } = useMutation({
  mutationFn: addTodo,
  onSuccess() {
    return refetch()
  }
})

但 TanStack Query 的核心維護æˆå“¡ï¼ˆ@TkDodo)ä¾ç„¶æŽ¨è–¦ä½¿ç”¨ invalidateQueriesï¼Œæ›´å‹æ–¼ refetchï¼Œåƒæ˜¯é€™ç¯‡è¨Žè«–或是下列這篇推文串。

é‡å°é€™å€‹å•題,核心維護æˆå“¡ä¹Ÿé€²ä¸€æ­¥æåˆ°äº†å…©å€‹ç†ç”±ï¼š

å¦å¤–,如果我們回顧å‰é¢çš„內容會發ç¾ï¼Œå¦‚æžœæˆ‘å€‘è¦ invalidate çš„ query 是ç¦ç”¨ç‹€æ…‹çš„話,TanStack Query åªæœƒæŠŠé€™å€‹ query æ¨™è¨˜ç‚ºç„¡æ•ˆï¼Œä¸æœƒé‡æ–°ç™¼é€è«‹æ±‚。這個細節 refetch å°±ä¸å®¹æ˜“åšåˆ°ï¼Œå¦‚æžœé¸ç”¨ refetchï¼Œå°±ç®—å°æ‡‰åˆ°çš„ query 是ç¦ç”¨ç‹€æ…‹ä¹Ÿæœƒé‡æ–°ç™¼é€è«‹æ±‚。這個行為ä¸ç®—是錯誤,這個行為被èªå®šç‚ºåªæ˜¯ç‚ºäº†ç¹žéŽ enabled 的一種手段。

çµèªž

在這篇文章中,我們了解了 invalidateQueries 的功能與使用場景,接著深入剖æžäº†åœ¨å‘¼å« invalidateQueries 後發生了什麼事。最後也比較了 invalidateQueries å’Œ refetch 兩種方法的差異跟官方推薦的使用方å¼ã€‚

useMutation Flow Chart - by Alex Liu

深入了解 invalidateQueries 之後,我們發ç¾å…¶æ•´é«”å¯¦ä½œé æ¯”é æœŸçš„è¦ç°¡å–®ã€‚除了原始碼外,會特別挑 invalidateQueries 出來寫的å¦ä¸€å€‹åŽŸå› æ˜¯åœ¨å·¥ä½œä¸Šè »å¸¸é‡åˆ°é¸ç”¨ invalidateQueries 與 refetch 的討論。很顯然地 refetch ä¸è«–在使用上跟命å上都比 invalidateQueries 更直覺,如果想è¦èªªæœåœ˜éšŠé¸ç”¨ invalidateQueries å°±éœ€è¦æ›´å®Œæ•´çš„論述。

到這裡就是關於 TanStack Query çš„ invalidateQueries çš„å®Œæ•´æŽ¢è¨Žæ‹‰ï¼æ–‡ç« ä¸­æœ‰ä»»ä½•想進一步了解或內容有誤的地方都歡迎跟我討論。

åƒè€ƒè³‡æ–™

è«‹æˆ‘å–æ¯å’–å•¡

如果這裡的內容有幫助到你的話,一æ¯å’–å•¡å°±æ˜¯å°æˆ‘最大的鼓勵。

è«‹æˆ‘å–æ¯å’–å•¡
ÿÿÿÿ