🍊

TanStack Queryのrefetchとenabledについて誤解してた話

2024/12/25に公開

これは、日本CTO協会24卒 Advent Calendar 2024 の21日目の記事です!(遅れましたmm)
https://qiita.com/advent-calendar/2024/ctoa-24

疑問に思ったコード

下記のコードを読んでいて、
enabled: falseのクエリをrefetch()しているのに、なんでクエリが実行できるんだろう🤔」
そう思いました。

enabledがクエリの実行を制御するものだと理解していたため、falseを指定したクエリをrefetchしたら、実行できないと思っていたからです。

const query = useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers,
  enabled: false
});

// どこかで実行
query.refetch();  // これが動く!?

分かったこと

1. refetch()はenabledを無視する

refetchの実装を見ると

// packages/query-core/src/queryObserver.ts
refetch({ ...options }: RefetchOptions = {}): Promise<
  QueryObserverResult<TData, TError>
> {
  return this.fetch({
    ...options,
  })
}

enabledのチェックがないため、そのままfetchを実行していました。

2. refetchQueriesはenabledを尊重する

一方、refetchQueriesの実装では

// packages/query-core/src/queryClient.ts
refetchQueries(...) {
  const promises = notifyManager.batch(() =>
    this.#queryCache
      .findAll(filters)
      .filter((query) => !query.isDisabled())  // ここで除外!
      .map((query) => query.fetch())
  )
}

https://github.com/TanStack/query/blob/4f90639610fdbd2135f5f726b123dd918682ff92/packages/query-core/src/queryClient.ts#L365-L393

isDisabled()でフィルタリングされていました。

3. なぜ違うのか?

isDisabledの実装を見ると

// packages/query-core/src/query.ts
isDisabled(): boolean {
  if (this.getObserversCount() > 0) {
    return !this.isActive()
  }
  return (
    this.options.queryFn === skipToken ||
    this.state.dataUpdateCount + this.state.errorUpdateCount === 0
  )
}

isActive(): boolean {
  return this.observers.some(
    (observer) => resolveEnabled(observer.options.enabled, this) !== false
  )
}

https://github.com/TanStack/query/blob/4f90639610fdbd2135f5f726b123dd918682ff92/packages/query-core/src/query.ts#L252-L267

enabledを見て、除外していたことがわかりました。

3. v3からv4での変更

調べてみると、v3まではrefetchQueriesは無効化されたクエリも実行していたようです。

https://github.com/TanStack/query/issues/3202
https://github.com/TanStack/query/pull/3223

v3までの動作

v3まではenabledの状態に関係なく、以下のメソッドで全てのクエリが再フェッチされていました。

// v3: enabled: falseでもリフェッチされる
queryClient.refetchQueries()
queryClient.invalidateQueries()

これにより、意図しないデータフェッチが発生する可能性がありました。
「あるクエリを意図的にenabledをfalseにしているのに、システム全体のリフェッチで予期せずフェッチされてしまう」なんてことが起きていました。

v4での改善

v4ではrefetchQueriesの実装が変更され、無効化されたクエリは明示的にスキップされるようになりました。

// v4の実装
const promises = notifyManager.batch(() =>
  this.#queryCache
    .findAll(filters)
    .filter((query) => !query.isDisabled())  // この行が追加された
    .map((query) => query.fetch())
)

この変更により、refetchQueriesは無効化されたクエリをスキップするようになり、意図しないフェッチの防止し、パフォーマンスの改善(不要なフェッチの削減)を実現することができました。

4. enabledについて

TanStack Queryのメンテナーは以下のように説明していました

Strictly speaking, a query itself cannot be enabled or disabled - it can just "be". Observers can be disabled if they shouldn't trigger requests by themselves, that's all.

enabled is an observer level property, which means you can call useQuery with the same key twice in two components, both being differently enabled.

訳:

  • 厳密に言えば、クエリ自体に有効/無効という状態はありません - それは単に"存在する"だけです。Observerは、自身でリクエストをトリガーすべきでない場合に無効化されるだけです。
  • enabledはobserverレベルのプロパティです。つまり、同じキーに対して2つのコンポーネントから異なるenabledを持つuseQueryを呼び出すことができます。

https://github.com/TanStack/query/issues/4761#issuecomment-1372249277

この説明から分かることは

  • enabledはクエリではなく、observerの性質
  • 個別のrefetch()は意図的な手動実行
  • refetchQueriesは自動的な一括処理
  • enabledはあくまでobserverの自動フェッチを制御するためのもの

まとめ

TanStack Queryのrefetchとenabledについて、下記のように整理することができました。
refetchやrefetchQueries, enabledをしっかり使いこなせるようにしていきたいです!

  • クエリ自体に「有効/無効」の状態はなく、enabledはオブザーバーの挙動を制御するのみ
  • refetch()enabledの設定に関わらず、手動でのデータ再取得を可能にする

参考

Discussion