until とは
until は、リアクティブなデータが特定の条件を満たすまで監視するためのユーティリティ関数。
監視対象が条件を満たすまで監視を続け、一度条件を満たしたらそれ以上の不要な監視は行われない。
例えば以下のようなコード。
const booleanRef = ref(false)
const stopWatch = watch(booleanRef, (newVal) => {
if (newVal) {
console.log('booleanRef is true now!')
stopWatch()
}
}, {
immediate: true,
})
これは以下のように書ける。
import { until } from '@vueuse/core'
const booleanRef = ref(false)
until(booleanRef).toBe(true).then(() => {
console.log('booleanRef is true now!')
})
または invoke を使ってawaitも可能。
import { until, invoke } from '@vueuse/core'
const booleanRef = ref(false)
invoke(async () => {
await until(booleanRef).toBe(true)
console.log('booleanRef is true now!')
})
invokeはただ即時実行関数をラップするユーティリティ関数。
実装を見る
実装はこちら。packages/shared/until/index.ts
untilを呼び出すと、UntilValueInstanceというインスタンスが返ってくる(簡単のため、配列を渡したパターンを除く)。UntilValueInstanceはtoBe,toMatchという核となるマッチャーを持っており、これらが Promise を返す。toBeTruthyなどのマッチャーもそれらを元に実装されている- これらのマッチャーは watch の戻り値である停止関数を保存しており、条件を満たしたらそれを呼び出して監視を停止してから Promise を resolve する
UntilValueInstanceはnotというゲッターを持っており、このゲッターを呼び出すと内部にフラグが保存され、マッチャーの条件達成が反転することでuntil(booleanRef).not.toBe(true)のように非定型でマッチャーを呼び出せる。- タイムアウトのオプションが指定されていた場合、マッチャーは Promise を返す際に、
Promise.raseに入れて返すことで、タイムアウト処理を実現している
いたってシンプル。
使い所
例えば以下。
import { until } from '@vueuse/core'
type ConfirmModalParams = {
title: string
onConfirm?: () => void
// その他必要なパラメータ
}
export const useConfirmModal = () => {
const showState = useState<boolean>('showConfirm', () => false)
const confirmParamsState = useState<ConfirmModalParams | null>('confirmParams', null)
const show = (params: ConfirmModalParams) => {
confirmParamsState.value = params
showState.value = true
}
const close = () => {
confirmParamsState.value = null
showState.value = false
}
const confirm = async (params: ConfirmModalParams): Promise<boolean> => {
const { resolve, promise } = Promise.withResolvers<boolean>()
show({
...params,
onConfirm: () => {
resolve(true)
close()
},
})
// いかなる方法で閉じられても Promise を解決する
until(showState)
.toBe(false)
.then(() => {
resolve(false)
})
return promise
}
return {
showState,
confirmParamsState,
confirm,
}
}
こんな感じで useConfirmModal という composable を作り、この showState と連動してモーダルを表示するようにしておくと、以下のように利用できる。
const { confirm } = useConfirmModal()
const onClickDelete = async () => {
const ok = await confirm({
title: '本当に削除しますか?',
})
if (!ok) {
return
}
deleteItem()
}
このように confirm の結果を Promise で受け取れるようになるため、 onConfirm onCancel のようなコールバック関数を用意する必要がなくなり、コードがシンプルになる。
ここで、showState と連動して表示されるコンポーネントでは、キャンセルボタンを押すだけではなく、エスケープキーだったり、モーダルの外側をクリックするなど、さまざまな方法でそれを閉じるような実装ができる。いかなる方法で閉じられても Promise は確実に解決され、呼び出し側はそれを待つだけでよい。
ちなみに、このような「UI を Promise で待つパターン」は createTemplatePromise や useConfirmDialog などでも実現できる。
まとめ
VueUse は別に Vue を使ってなくても利用できるユーティリティ関数がたくさんあって便利。
そもそも、Vue 自体が ref や watch などのリアクティビティに関する実装を @vue/reactivity として明確に分離して提供しているため、Vue を使わないプロジェクトでも @vue/reactivity と VueUse を組み合わせて利用できる。