🚫

Vue Query(TanStack)をNuxt2で使うと危険!?

2023/07/28に公開

Reactで広く使われているTanStack Queryには、Vue版があり、Nuxt.jsでも使用することができます。

https://tanstack.com/query/latest/docs/vue/overview

ですが、 公式ドキュメント の通りにNuxt2で使用すると、キャッシュがリクエストを跨いで保持されてしまうという重大な問題があることが分かりました。

→ 結論としては、現状はNuxt2ではVue Queryは使わない方が良さそうです。

バージョン

  • nuxt 2.17.1(2.16でも同様)
  • @tanstack/vue-query 4.32.0
  • @nuxtjs/composition-api 0.33.1

問題の例

plugins/vue-query.js に以下のように設定します。

plugins/vue-query.js
import { hydrate, VueQueryPlugin, QueryClient } from "@tanstack/vue-query";
import Vue from "vue";

export default (context) => {
  const queryClient = new QueryClient({
    defaultOptions: {
      queries: { staleTime: 5 * 60 * 1000 }, // 5分
    },
  });
  const options = { queryClient };

  Vue.use(VueQueryPlugin, options);

  if (process.client) {
    if (context.nuxtState && context.nuxtState.vueQueryState) {
      hydrate(queryClient, context.nuxtState.vueQueryState);
    }
  }
};

ページ内で以下のように使用します。

pages/sample.vue
<script setup>
import { dehydrate, useQuery, useQueryClient } from "@tanstack/vue-query";
import { onServerPrefetch, useContext } from "@nuxtjs/composition-api";

const { data, suspense } = useQuery(["test-key"], () =>
  Promise.resolve(new Date().toLocaleString())
);

const { ssrContext } = useContext();
const queryClient = useQueryClient();

onServerPrefetch(async () => {
  await suspense();

  ssrContext.nuxt.vueQueryState = dehydrate(queryClient);
});
</script>

<template>
  <div>{{ data }}</div>
</template>

すると、何回リロードしても、最初にキャッシュされた値が表示されてしまいます。

これは、別ブラウザなどでアクセスしても、同じキャッシュが表示されてしまうため、もし個人情報を含むリクエストをキャッシュしていた場合、他の人にその値が表示されるという重大な問題になってしまいます。

hydrateを使わない場合

hydrateなどを使用せず、クライアント側だけで利用するとします。

plugins/vue-query.js
// if (process.client) {
//   if (context.nuxtState && context.nuxtState.vueQueryState) {
//     hydrate(queryClient, context.nuxtState.vueQueryState);
//   }
// }

その場合、リロードごとに値が更新されるように見えますが、SSR時にはキャッシュは使われるため、mounted前にキャッシュが見えたり、HTMLソース上にはしっかりキャッシュが載ってしまうため、注意が必要です。

完全にクライアント側だけで利用したい場合は、enabledをクライアント側のみtrueにする必要があります。

pages/sample.vue
const { data } = useQuery(["test-key"], () =>
  Promise.resolve(new Date().toLocaleString()),
  { enabled: process.browser }
);

対策してみた(失敗)

cacheTimeやstaleTimeを0にしても、フェッチする前にキャッシュが残っていて、危険な状態には変わりがありませんでした。


また、以下のようにキャッシュをクリアする処理を入れると、

pages/sample.vue
...

const { beforeSerialize, ssrContext } = useContext();
const queryClient = useQueryClient();

...

// SSRの最後にキャッシュをクリアする
beforeSerialize?.(() => {
  queryClient.clear()
})

一見成功したように見えましたが、

もし同じタイミングでリクエストがあった場合、他の人のキャッシュに影響が出てしまうようでした。

Nuxt3の場合

Nuxt3の場合は、 ドキュメント の通りに使うと、ちゃんとキャッシュはリクエスト内でのみ有効になっていました。

結論

ドキュメントには、「リクエストが終わったらメモリはクリアされる」とあるため、Nuxt2だけバグがあるという状況のようです。

On the server, cacheTime defaults to Infinity which disables manual garbage collection and will automatically clear memory once a request has finished.

時間がある時にIssueを出してみたいと思います。

Discussion