🟡

【Vue.js】状態管理編[ref/reactive/v-model/compted/watch]+Pinia

に公開

ref()

ref()の実装例と解説
countという変数をref関数で監視している
<template>
  <p>カウント: {{ count }}</p>
  <button @click="increment">+1</button>
        // ↑ ボタンにクリックアクションが行われた際にincrement関数が呼び出されるよう指定  
</template>

<script setup>
// import文でref関数を使用するという宣言を行う 
import { ref } from 'vue'

// 初期値が0であると宣言
// 「数値」「文字列」「Boolean」など、1つのデータを使いたいときに使う。
const count = ref(0)

// ボタンがクリックされると以下の処理が実行される
const increment = () => {
  count.value++  // 表示・操作するときは .value が必要
}
</script>

reactive()

reactive()の実装例と解説
reactive() を使うことで、user.nameやuser.ageの個別のプロパティがVueにより監視される
<template>
  <p>名前: {{ user.name }}</p>
  <p>年齢: {{ user.age }}</p>
  <button @click="changeName">名前変更</button>
    // ↑ ボタンにクリックアクションが行われた際にchangeName関数が呼び出されるよう指定
</template>

<script setup>
// import文でreactive関数を使用するという宣言を行う 
import { reactive } from 'vue'

// 複数のデータをまとめて管理したいときに便利
const user = reactive({
  name: '山田',
  age: 25
})

// 山田 → 田中へ変更
const changeName = () => {
 user.name = '田中'
 user.age = 30
}
</script>

v-model

v-modelの実装例と解説


バナナ・ぶどうのチェックボックスの状態が管理されているため、下に選択したものが表示される)

v-modelの実装例(複数のチェックボックスの状態を同期する)
<template>
  <div>
    <h3>好きなフルーツを選んでください:</h3>
    <label><input type="checkbox" value="りんご" v-model="fruits" /> りんご</label>
    <label><input type="checkbox" value="バナナ" v-model="fruits" /> バナナ</label>
    <label><input type="checkbox" value="ぶどう" v-model="fruits" /> ぶどう</label>
                                                // ↑ v-modelでバインディング

    <p>選んだフルーツ: {{ fruits.join(', ') }}</p>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const fruits = ref([])
    // ↑ fruitsという変数がref関数も併用して双方向にバインディングされている状態
    // ↑ もし、ref(['りんご','ぶどう'])とすると(双方向に管理しているため)デフォルトでチェックボックスにチェックが入る 
</script>

compted()

compted()の実装例と解説
comptedの実装例(複数のチェックボックスの状態を同期する)
<template>
  <div>
    <p>身長(cm): <input v-model.number="height" /></p>
    <p>体重(kg): <input v-model.number="weight" /></p>

    <p>
      BMI:
      <span v-if="bmi !== null">{{ bmi }}</span>
    </p>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue'

const height = ref('')
const weight = ref('') 

const bmi = computed(() => {
  if (!height.value || !weight.value || height.value === 0) return null

  const heightInMeters = height.value / 100 // ← cm → m に変換
  return (weight.value / (heightInMeters ** 2)).toFixed(2)
})
</script>

watch()

watch()の実装例と解説
watchの実装例(BMIの計算結果の値を元に処理を実装する)
<template>
  <div>
     <h1>watch()</h1>
    <p>身長(cm): <input v-model.number="height_w" /></p>
    <p>体重(kg): <input v-model.number="weight_w" /></p>

    <p>
      BMI:
      <span v-if="bmi !== null">{{ bmi_w }}</span>
    </p>

    <p v-if="bmiCategory" style="color: blue;">
      肥満度: {{ bmiCategory }}
    </p>
  </div>
</template>

<script setup>
import { ref, computed, watch } from 'vue'

const height_w = ref('')
const weight_w = ref('')
const bmi_w = computed(() => {
  if (!height_w.value || !weight_w.value || height_w.value === 0) return null

  const heightInMeters = height.value / 100
  return (weight_w.value / (heightInMeters ** 2)).toFixed(2)
})
const bmiCategory = ref('')

// BMI を watch(computed の結果を監視)
watch(bmi_w, (newBmi) => {
  if (!newBmi) {
    bmiCategory.value = ''
    return
  }

  const numericBmi = parseFloat(newBmi)

  if (numericBmi < 18.5) {
    bmiCategory.value = '低体重(やせ)'
  } else if (numericBmi < 25) {
    bmiCategory.value = '普通体重'
  } else if (numericBmi < 30) {
    bmiCategory.value = '肥満(1度)'
  } else if (numericBmi < 35) {
    bmiCategory.value = '肥満(2度)'
  } else if (numericBmi < 40) {
    bmiCategory.value = '肥満(3度)'
  } else {
    bmiCategory.value = '肥満(4度)'
  }
})
</script>

今回学んだことを組み合わせると...

身長・体重を入力 → BMIを表示 → 一定値を超えたら注意を出す
[ref/reactive]  ←→  [v-model: 入力フォームと連携][computed]  ← 状態を加工してUIに表示
     ↓
[watch/watchEffect]  ← 状態変化をきっかけに処理を実行
(例)BMI計算ツール
BMI計算ツール
<template>
  <div>
    <h2>BMI計算ツール</h2>

    <label>
      身長(cm):
      <input type="number" v-model="height" />
    </label>

    <br />

    <label>
      体重(kg):
      <input type="number" v-model="weight" />
    </label>

    <br />

    <p>BMI: <span v-if="bmi !== ''">{{ bmi }}</span></p>
    <p style="color: red;" v-if="warning">{{ warning }}</p>
  </div>
</template>

<script setup>
import { ref, computed, watch } from 'vue'

// リアクティブな入力値
const height = ref('')
const weight = ref('')

// BMI計算(computedでキャッシュ)
const bmi = computed(() => {
  const h = parseFloat(height.value)
  const w = parseFloat(weight.value)

  if (!h || !w || h === 0) return ''
  return (w / ((h / 100) ** 2)).toFixed(2)
})

// 警告メッセージ表示(watch)
const warning = ref('')

watch(bmi, (newVal) => {
  const numericBmi = parseFloat(newVal)
  if (numericBmi >= 25) {
    warning.value = 'BMIが高いです(肥満傾向)'
  } else if (numericBmi < 18.5 && newVal !== '') {
    warning.value = 'BMIが低いです(やせ型)'
  } else {
    warning.value = ''
  }
})
</script>

[発展]Pinia(状態管理ライブラリ)を用いる

pinia無 vs Pinia有
Pinia無(Viewの記述が肥大化してしまう)
<script setup>
import { ref, computed } from 'vue'

const height = ref('')
const weight = ref('')
const bmi = computed(() => {
  const h = parseFloat(height.value)
  const w = parseFloat(weight.value)
  return h ? (w / ((h / 100) ** 2)).toFixed(2) : ''
})
</script>

<template>
  <input v-model="height" />
  <input v-model="weight" />
  <p>BMI: {{ bmi }}</p>
</template>
Pinia有(ロジックをStoreに分離させることでViewが肥大化せず済む)
<script setup>
import { useBmiStore } from '@/stores/useBmiStore'
const bmiStore = useBmiStore()
</script>

<template>
  <input v-model="bmiStore.height" />
  <input v-model="bmiStore.weight" />
  <p>BMI: {{ bmiStore.bmi }}</p>
</template>
src/stores/useBmiStore.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useBmiStore = defineStore('bmi', () => {
  const height = ref('')   // 入力値(cm)
  const weight = ref('')   // 入力値(kg)

  const bmi = computed(() => {
    const h = parseFloat(height.value)
    const w = parseFloat(weight.value)
    if (!h || !w) return ''        // 空欄のときは表示しない
    const heightInMeters = h / 100
    return (w / (heightInMeters ** 2)).toFixed(2)
  })

  return {
    height,
    weight,
    bmi
  }
})

Discussion