📚

Vuetify3(beta)のv-itemで@group:selectedを使うのは気をつけた方がいい話

2022/10/04に公開

この記事は

  • v-itemの@group:selectedを使ったらcallbackの無限ループにハマったお話

前提条件

  • nuxt:3.0.0-rc.11
  • vuetify:3.0.0-beta.11

何をやろうとしたか

  1. こんな感じのタグが並んでるコンポーネント(tagChips.vue)を作ってました。
    tagchips

  2. このコンポーネントはprops.modeで'select'と'link'の二つの値を取り、それによって挙動が変わるようになっていました。

    • mode = 'select'のとき
      • 左二つは非選択状態、真ん中はホバー時、右二つは選択状態といった感じです
      • この状態では、タグをクリックした時に選択・非選択状態だけを切り替え、後述のようにリンクとして飛んだりはしません
      • タグ全体をtagsで持ち、選択中のタグをcheckedTagという変数で受け取っていました。
        tagchips
    • mode = 'link'のとき
      • この状態では選択中のタグのみ表示し、クリックした時にはそのタグに対応したページ(サイト内)に飛ぶ仕様になっていました。
        tagchips
  3. そして、問題が発生したページでは、mode = 'select'に指定したtagChipsとmode = 'link'に指定したtagChipsをそれぞれ一つずつ表示していました。

具体的に何が起きたか

  • 問題が発生したページで、mode = 'select'に指定したtagChipsの選択中のタグを非選択状態に戻そうとクリックするとcallbackの無限ループが発生し、「Maximum call stack size exceeded」のエラー文と共にスタックしてしまいました。

問題のコンポーネント

  • デザイン周りを省略して処理周りだけ抜き出すと、
tagChips.vue
<script setup lang="ts">
import { TagType } from '~~/assets/types'

const Props = withDefaults(
  defineProps<{
    tags: TagType[]
    checkedTag: TagType[]
    mode: 'select' | 'link'
  }>(),
  {
    tags: () => [],
    checkedTag: () => [],
    mode: 'link'
  }
)
const select = (item: TagType) => {
  if (Props.mode === 'link') navigateTo('/tag/' + item.id)
  else if (Props.mode === 'select') item.is_active = !item.is_active
}
</script>
<template>
  <v-item-group
    :model-value="checkedTag"
    @update:model-value="$emit('update:checked-tag', $event)"
  >
    <v-item
      v-for="item in tags"
      v-slot="{ isSelected, toggle }"
      :value="item"
      @group:selected="select(item)"
    >
      <v-hover v-slot="{ isHovering, props }">
        <v-sheet
          {中略}
          v-bind="props"
          @click="toggle"
        >
          {中略}
        </v-sheet>
      </v-hover>
    </v-item>
  </v-item-group>
</template>

このような実装になっていました。

恐らく何が起きていたか

vuetifyの内部実装まで確認した訳ではないので、推測の域を出ないのですが、

  1. mode = 'select'に指定したtagChipsのタグをクリックしたことによって@click="toggle"のtoggle関数が発火
  2. リアクティブになっているcheckedTagからtagが一つ削除される
  3. checkedTagが変化したので、@group:selectedが発火する
  4. @group:selectedに設定したselect関数の中でis_activeという変数のtrue/falseが入れ替わる
  5. それによって削除したはずtagがのcheckedTagに再度追加される
  6. checkedTagが変化したので、@group:selectedが発火する
  7. @group:selectedに設定したselect関数の中でis_activeという変数のtrue/falseが入れ替わる
  8. それによって削除したはずtagがのcheckedTagから再度削除される
  9. 以下3に戻る
    を繰り返していたと考えています。

改善するためにどう修正したか

  • 今回は、@group:selectedを使うのをやめて、@clickで処理をまとめることにしました。
  • toggle関数は型定義を見ると() => voidとあるので、toggle関数を引数に取り、select関数の中の実行したい箇所で発火させるように変更します。
tagchips.vue
<script setup lang="ts">
{中略}
const select = (item: TagType, toggle?: () => void) => {
  if (Props.mode === 'link') navigateTo('/tag/' + item.id)
  else if (Props.mode === 'select') {
    if (toggle) toggle()
    item.is_active = !item.is_active
  }
}
</script>
<template>
  <v-item-group
    :model-value="checkedTag"
    @update:model-value="$emit('update:checked-tag', $event)"
  >
    <v-item v-for="item in tags" v-slot="{ isSelected, toggle }" :value="item">
      <v-hover v-slot="{ isHovering, props }">
        <v-sheet
          {中略}
          v-bind="props"
          @click="select(item, toggle)"
        >
          {中略}
        </v-sheet>
      </v-hover>
    </v-item>
  </v-item-group>
</template>

以上になります。
もし別の回避方法を見つけた方がいらっしゃいましたら、コメントにて頂けると幸いです。

GitHubで編集を提案

Discussion