t28.dev

Vue の reactivity を使った tanstack/vue-query v5 の制御方法検討のために、実装・文書・PRを漁る

2024/9/6
Scrap

@tanstack/vue-query v4 を使って WebAPI を呼び出す hook を実装していたとき、 WebAPI 用のパラメーター(paramRef)の状態 (string or undefined) に応じて enabled オプションで API の呼び出しを制御していた。

切り替えたいオプションは enabled だけだけれど、queryKeyqueryFnparamRef に依存している (undefined のときは実行させたくないし、型も不正)。 そのため、useQuery のオプションを丸ごと computed していた。

export const useApi = (paramRef: Ref<string | undefined>) => {
  return useQuery(
    computed(() => {
      const param = paramRef.value;
      return !param
        ? { enabled: false }
        : {
            queryKey: createQueryKey(param),
            queryFn: () => callApi(param),
          };
    }),
  );
};

@tanstack/vue-query v5 に更新したところ、型エラーが出るようになってしまった。解決するためには、createQueryKey(param) と同じ、またはサブセットの型を定義する必要があった。

export const useApi = (paramRef: Ref<string | undefined>) => {
  return useQuery(
    computed(() => {
      const param = paramRef.value;
      return !param
        ? {
            enabled: false,
            queryKey: ["key", "__DUMMY__"] as const, // 👈 ???!!!!
          }
        : {
            queryKey: createQueryKey(param), // これの型が readonly ["key", string]
            queryFn: () => callApi(param),
          };
    }),
  );
};

まぁ、嫌だ…。 使い方が間違っているはずだし、ドキュメント等を読んでみる。

https://tanstack.com/query/latest/docs/framework/vue/reference/useQuery

enabled: boolean | (query: Query) => boolean

  • Set this to false to disable this query from automatically running.
  • Can be used for Dependent Queries.

enabled に関数を渡すことで、 callback で query を受け取って true/false を決められそう、って思ったけれど、setup 時に query が undefined になる。

query が型に反して undefined になるのは、vue-query が query インスタンスを引数で渡していないから。

https://github.com/TanStack/query/blob/v5.55.4/packages/vue-query/src/useBaseQuery.ts#L90

Issue も出来ていた。 vue-query の lifecycle 的に setup 時に query instance を取得するのは厳しそう。型の問題もあるらしい (as any 使ってるぐらいだしな…)

[vue-query] Callback for enabled does not provide query as parameter #7905

ただし、enabled オプションに関数を渡すのは正しいっぽい。'should be enabled to accept getter function' っていうテストコードもある。

https://github.com/TanStack/query/blob/v5.55.4/packages/vue-query/src/__tests__/useQuery.test.ts#L271

ドキュメントの別の記述だと、enabled には computedRef を渡している。

https://tanstack.com/query/v5/docs/framework/vue/guides/disabling-queries#lazy-queries

とりあえず分かったのは、vue-query の api reference は当てにならないっぽいこと…。Vue と React だと API も 内部の実装も結構違うのに API Document が共通になっている。

https://github.com/TanStack/query/blob/main/docs/framework/vue/reference/useQuery.md

enabled オプションの実装タイミングを掘ってみると、結構ややこしい状況になっている。

Add possibility to pass a callback to enabled. #7566 での query-core に対する修正で enabled オプションが関数を受け付けるようになった。 これは v5.48.0 でリリースされている 一方、 vue-query は v5.47.0 の時点で既に enabled に関数を渡せるようになっていた。

feat(vue-query): let composables accepts enabled as a getter function #6018 で computed だけでなく getter が受け取れるように修正されている。

// Before
useQuery({
  queryKey: ["TODO"],
  queryFn: () => Promise.resolve({ data: [] }),
  enabled: computed(() => isMounted.value && isAuth.value),
});
// After
useQuery({
  queryKey: ["TODO"],
  queryFn: () => Promise.resolve({ data: [] }),
  enabled: () => isMounted.value && isAuth.value,
});

つまり、enabled オプションは package 毎にまったく別のものが提供されている…。

  • query-core (または react-query) における query を引数にした callback
  • vue-query における Vue getter

Vue getter は一般的な getter/setter とは異なる概念みたいだ。これは別の機会に調べよう (https://blog.vuejs.org/posts/vue-3-3#better-getter-support-with-toref-and-tovalue)

結局どうすんの

paramRef に依存している queryKey 問題

queryKey オプションは MaybeRef 型なので、Ref<string | undefined> のまま渡しちゃう

paramRef に依存している queryFn 問題

skipToken が便利そう

https://tanstack.com/query/v5/docs/framework/vue/guides/disabling-queries#typesafe-disabling-of-queries-using-skiptoken

export const useApi = (paramRef: Ref<string | undefined>) => {
  return useQuery({
    queryKey: createQueryKey(paramRef),
    queryFn: computed(() => {
      const param = paramRef.value;
      if (!param) {
        return skipToken; // 👈 `enabled` オプションの代わりに `skipToken` を返して disable にする
      }
      return () => callApi(param); // 👈 param が string なら API を実行する
    }),
  });
};