🐙

【Nuxt3+@urql/vue】useQueryでも異なるvariablesでexecuteQueryしたい

2023/09/19に公開

結論

できない

ので、代替策を考えていきます。

環境

node: 18.17.1
nuxt: 3.6.5 (vue: 3.3.4)
@urql/vue: 1.1.2
@graphql-codegen/cli: 5.0.0
@graphql-codegen/client-preset: 4.1.0

サンプルアプリを用意しました。
https://github.com/ikumi-rai/nuxt-urql

useMutationについて

まずはこの話の前提となるuseMutation()について使い方を見ていきましょう。

.vue
<template>
  <button @click="setLiftStatus('astra-express', 'OPEN')">OPEN</button>
  <button @click="setLiftStatus('astra-express', 'CLOSED')">CLOSED</button>
  <!-- .dataや.error、.fetchingなどは全てリアクティブ -->
  {{ setLiftStatusRes.data }}
</template>

<script setup lang="ts">
import { LiftStatus } from "graphql/generated/graphql"

const setLiftStatusRes = useMutation(
  graphql(`
    mutation setLiftStatus($id: ID!, $status: LiftStatus!) {
      setLiftStatus(id: $id, status: $status) {
        id
        status
      }
    }
  `),
)

const setLiftStatus = (id: string, status: LiftStatus) => {
  setLiftStatusRes.executeMutation({
    id: id,
    status: status,
  })
}
</script>

このようにsetup内でuseMutation()を使用し、戻り値であるUseMutationResponseのexecuteMutation()にvariablesを渡すことで同じmutationを異なる変数で何度も使用することが出来ます。

これをuseQuery()でもやりたいという話ですね。

なぜできないのか?

上記サンプルアプリケーションではUseQueryResponseを使いまわしたい代表的なユースケースであるインクリメンタルサーチを実装しています。


これが

こうなるやつ

ではコードを確認していきます。

.vue
<template>
  <input type="text" @input="searchLift($event.target.value)" />
  <MyList :data=searchLiftRes.data />
</template>

<script setup lang="ts">
const searchLiftRes = useQuery({
  query: graphql(`
    query searchLift($id: String!) {
      search(term: $id) {
        ... on Lift {
          ...LiftList_lift
        }
      }
    }
  `),
  variables: { id: "" },
  pause: true, // `useQuery()`は`useMutation()`と異なりOperationが即時発行されるためpauseで止める
})

const searchLift = (id: string) => {
  searchLiftRes.executeQuery({ // `executeQuery()`はpauseの値を無視して強制的にOperationを発行する
    variables: { id: id },
  })
}
</script>

useMutation()の例と照らし合わせてみても一見ぽいコードになっているように思えますが、これは期待通りには動作しません。
なぜならexecuteMutation()の引数が(発行される)Operationのvariablesを指定するものであるのに対してexecuteQuery()の引数はOperationのcontextを上書きするものであるからです。

実際に上記のコードでsearchLift()を走らせると以下のようなOperationが発行されます。

Operation
{
  context: {
    requestPolicy: "cache-first",
    url: "https://snowtooth.moonhighway.com/",
    ...
    variables: { id: "w" }, // ← 未知のcontextなので無視される
  },
  key: ...,
  kind: ...,
  query: ...,
  variables: { id: "" }, // ← 実際に`query searchLift`に渡される引数
}

つまりそもそもexecuteQuery()そのような用途で使うものではないということですね。
ではこれは一体なんなのかと言うと、TSDocにその答えが書いてあります。

This is useful for re-executing a query and get a new network result, by passing a new request policy.

const result = useQuery({ query });

const refresh = () => {
  // Re-execute the query with a network-only policy, skipping the cache
  result.executeQuery({ requestPolicy: 'network-only' });
};

デフォルトではrequestPolicyは"cache-first"、つまりキャッシュがある場合にはリクエストを送らずにキャッシュを返す設定なので、"network-only"でcontextを上書きすることでデータをリフレッシュすることが出来ますよ、という話でした。

じゃあどうすればいいんだ!!

どうすればいいんだ?

考えうる方法としては以下の3つが挙げられると思います。

  1. useQuery()executeQuery()ではなく毎回useQuery()を使う
  2. useQuery()部分をコンポーネントに切り出す / 下位コンポーネントに委ねる
  3. useQuery()に渡すvariablesをリアクティブな値にする

このうち1についてはそもそも実現不可能です。use...()はsetupやライフサイクルフックの中でのみ使用することが出来ます。
2が恐らく最もurqlの思想と合致しているやり方で、事実公式ドキュメントでUIパターンとして紹介されている無限スクロールの項ではこの方法が採られています。が、Vueにおいては基本的にコンポーネントが増える=ファイルが増えるですのでこの方法とは噛みあいが悪いです。

というわけで現実的な路線[1]では3が妥当だと考えました。

variablesをリアクティブな値にする

useQuery()の全ての引数はリアクティブな値を受け入れます。通常であればOperationはexecuteQuery()によって明示的に発行あるいは再発行する必要がありますが、リアクティブな値を持つ場合は変更を検知し自動的に新しい値でOperationを再発行します

基本形.vue
<template>
  <input type="text" @input="searchLiftVars.id = $event.target.value" />
  <MyList :data=searchLiftRes.data />
</template>

<script setup lang="ts">
const searchLiftVars = ref({
  id: "",
})
const searchLiftRes = useQuery({
  query: graphql(`
    query searchLift($id: String!) {
      search(term: $id) {
        ... on Lift {
          ...LiftList_lift
        }
      }
    }
  `),
  variables: searchLiftVars,
  pause: computed(() => !searchLiftVars.value.id),
})
</script>

このコンポーネントの仕組みはこんな感じです。

  • 読み込み時: searchLiftVars.idは空でありpauseがtrueのためOperationは発行されない。
  • inputイベント発生時: searchLiftVars.idの値が入力値に変更され、pauseもfalseになる。この変更はVueによって検知され[2]、urqlクライアントがOperationを(再)?発行する。

これがリアクティブなvariablesの基本的な使い方になりますが、出来ればOperationの発行は自分で制御したいですよね。つまり理想形はこうであるはずです。

理想形.vue
<template>
  <input type="text" @input="searchLift($event.target.value)" />
  <MyList :data=searchLiftRes.data />
</template>

<script setup lang="ts">
const _searchLiftVars = ref({ // templateからは見て欲しくないという意思表示の`_`
  id: "",
})
const searchLiftRes = useQuery({
  query: graphql(...),
  variables: _searchLiftVars,
  pause: true, // `executeQuery()`によってのみOperationを発行できる
})

const searchLift = (id: string) => {
  _searchLiftVars.value.id = id
  searchLiftRes.executeQuery()
}
</script>

疑似的ではありますがexecuteMutation()っぽいコードになりました。ただしこのコードはまだ動きません。もうひと手間加える必要があります。

完成形.vue
<template>
  <input type="text" @input="searchLift($event.target.value)" />
  <MyList :data=searchLiftRes.data />
</template>

<script setup lang="ts">
const _searchLiftVars = ref({
  id: "",
})
const searchLiftRes = useQuery({
  query: graphql(...),
  variables: _searchLiftVars,
  pause: true,
})

const searchLift = (id: string) => {
  _searchLiftVars.value.id = id
  nextTick(searchLiftRes.executeQuery) // `nextTick()`で包む
}
</script>

基本形.vueではsearchLiftVarsの変更を検知してOperationを発行していましたが、理想形.vueでは_searchLiftVarsの変更を検知するexecuteQuery()が差し込まれてしまっています(=変更前の値でOperationが発行されます)。そこでnextTick()です。
nextTick()はPromiseを返すので、例えば以下のようにOperationの結果を使って別の処理を行うこともできます。

発展形.vue
<template>
  <input type="text" @input="searchLift($event.target.value)" />
</template>

<script setup lang="ts">
const _searchLiftVars = ref({
  id: "",
})
const searchLiftRes = useQuery({
  query: graphql(...),
  variables: _searchLiftVars,
  pause: true,
})

const searchLift = async (id: string) => {
  _searchLiftVars.value.id = id
  await nextTick(searchLiftRes.executeQuery)
  doSomething(searchLiftRes.data.value?.search)
}

const doSomething = (value) => {
  console.log(value)
}
</script>

いいですね。

まとめ

Vueコンポーネント内で同じクエリを異なる変数で複数回呼びたい場合、

  • setup内でuseQuery()を呼ぶ
  • その際variablesにはリアクティブな値を、pauseにはtrueを渡す
  • executeQuery()nextTick()で包む

これでuseMutation()と同様の使用感を得ることができました。
それでは!

脚注
  1. 「hackなことをしない」という意味 ↩︎

  2. 詳しくはソース↩︎

Discussion