📚
Vuetify3(beta)のv-itemで@group:selectedを使うのは気をつけた方がいい話
この記事は
- v-itemの@group:selectedを使ったらcallbackの無限ループにハマったお話
前提条件
- nuxt:3.0.0-rc.11
- vuetify:3.0.0-beta.11
何をやろうとしたか
-
こんな感じのタグが並んでるコンポーネント(tagChips.vue)を作ってました。
-
このコンポーネントはprops.modeで'select'と'link'の二つの値を取り、それによって挙動が変わるようになっていました。
- mode = 'select'のとき
- 左二つは非選択状態、真ん中はホバー時、右二つは選択状態といった感じです
- この状態では、タグをクリックした時に選択・非選択状態だけを切り替え、後述のようにリンクとして飛んだりはしません
- タグ全体をtagsで持ち、選択中のタグをcheckedTagという変数で受け取っていました。
- mode = 'link'のとき
- この状態では選択中のタグのみ表示し、クリックした時にはそのタグに対応したページ(サイト内)に飛ぶ仕様になっていました。
- この状態では選択中のタグのみ表示し、クリックした時にはそのタグに対応したページ(サイト内)に飛ぶ仕様になっていました。
- mode = 'select'のとき
-
そして、問題が発生したページでは、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の内部実装まで確認した訳ではないので、推測の域を出ないのですが、
- mode = 'select'に指定したtagChipsのタグをクリックしたことによって@click="toggle"のtoggle関数が発火
- リアクティブになっているcheckedTagからtagが一つ削除される
- checkedTagが変化したので、@group:selectedが発火する
- @group:selectedに設定したselect関数の中でis_activeという変数のtrue/falseが入れ替わる
- それによって削除したはずtagがのcheckedTagに再度追加される
- checkedTagが変化したので、@group:selectedが発火する
- @group:selectedに設定したselect関数の中でis_activeという変数のtrue/falseが入れ替わる
- それによって削除したはずtagがのcheckedTagから再度削除される
- 以下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>
以上になります。
もし別の回避方法を見つけた方がいらっしゃいましたら、コメントにて頂けると幸いです。
Discussion