ymmooot

VueUse の until が便利

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 というインスタンスが返ってくる(簡単のため、配列を渡したパターンを除く)。
  • UntilValueInstancetoBe, toMatch という核となるマッチャーを持っており、これらが Promise を返す。toBeTruthy などのマッチャーもそれらを元に実装されている
  • これらのマッチャーは watch の戻り値である停止関数を保存しており、条件を満たしたらそれを呼び出して監視を停止してから Promise を resolve する
  • UntilValueInstancenot というゲッターを持っており、このゲッターを呼び出すと内部にフラグが保存され、マッチャーの条件達成が反転することで 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 自体が refwatch などのリアクティビティに関する実装を @vue/reactivity として明確に分離して提供しているため、Vue を使わないプロジェクトでも @vue/reactivity と VueUse を組み合わせて利用できる。