🟡
【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