Vue — Composables
テクノロジ系 / L05 state-management — 状態管理
Vue — Composables
Composables = React の custom hook
// composables/useCounter.ts
import { ref } from "vue";
export function useCounter(initial = 0) {
const count = ref(initial);
const increment = () => count.value++;
const decrement = () => count.value--;
const reset = () => count.value = initial;
return { count, increment, decrement, reset };
}
// 使う
<script setup>
import { useCounter } from "@/composables/useCounter.js";
const { count, increment } = useCounter(0);
</script>「use」で始まる関数で 状態とロジックを共有。
命名規則
- 必ず
useで始まる (慣習) - camelCase (
useCounter,useLocalStorage) - 副作用の内容を名前で表す
useLocalStorage
import { ref, watch } from "vue";
export function useLocalStorage<T>(key: string, initial: T) {
const raw = localStorage.getItem(key);
const value = ref<T>(raw ? JSON.parse(raw) : initial);
watch(value, (newVal) => {
localStorage.setItem(key, JSON.stringify(newVal));
}, { deep: true });
return value;
}
// 使う
const theme = useLocalStorage("theme", "light");useFetch
import { ref, watchEffect } from "vue";
type AsyncState<T> =
| { status: "loading" }
| { status: "success"; data: T }
| { status: "error"; error: string };
export function useFetch<T>(urlGetter: () => string) {
const state = ref<AsyncState<T>>({ status: "loading" });
watchEffect(async (onCleanup) => {
const ctrl = new AbortController();
onCleanup(() => ctrl.abort());
state.value = { status: "loading" };
try {
const res = await fetch(urlGetter(), { signal: ctrl.signal });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
state.value = { status: "success", data: await res.json() };
} catch (err) {
if ((err as Error).name === "AbortError") return;
state.value = { status: "error", error: (err as Error).message };
}
});
return state;
}VueUse — 標準デファクト
$ pnpm add @vueuse/core
<script setup>
import { useLocalStorage, useMouse, useDebounceFn, useIntersectionObserver } from "@vueuse/core";
const theme = useLocalStorage("theme", "light");
const { x, y } = useMouse();
const debouncedSearch = useDebounceFn(search, 300);
</script>200+ の useful composable が揃っている。
自前で書く前にまず VueUse を見るべし。
Composables vs Pinia
| Composables | Pinia | |
|---|---|---|
| state スコープ | 呼び出した component | グローバル共有 |
| 用途 | ロジック共有 | アプリ全体 state |
| 使い分け | ローカル化 | グローバル化 |
両方使う。
- 個別 component で完結 → Composable
- 複数 component で共有 → Pinia
まとめ
use*関数でロジックを共有- React の custom hook と同じ思想
- VueUse に 欲しいものはほぼある
- ローカルは Composable、グローバルは Pinia
L05 完。次は L06 TypeScript連携。
考えてみよう
既定では正解・不正解の判定はしません。自分のペースで「答えを見る」を。採点してほしい場合だけ「採点してほしい」を押してください。
Lv1: 基本 — useCounter
問題
// composables/useCounter.ts
import { ref } from "vue";
export function useCounter(initial = 0) {
const count = ref(initial);
const inc = () => count.value++;
const dec = () => count.value--;
const reset = () => count.value = initial;
return { count, inc, dec, reset };
}<script setup>
import { useCounter } from "@/composables/useCounter.js";
const { count, inc, reset } = useCounter(0);
</script>複数 component で独立にカウント。
合格基準
- 同じ hook を再利用
- 状態は独立
Lv2: 応用 — useLocalStorage
問題
import { ref, watch } from "vue";
export function useLocalStorage<T>(key: string, initial: T) {
const raw = localStorage.getItem(key);
const value = ref<T>(raw ? JSON.parse(raw) : initial);
watch(value, (v) => localStorage.setItem(key, JSON.stringify(v)), { deep: true });
return value;
}
// 使う
const theme = useLocalStorage("theme", "light");リロード後も値が保持。
合格基準
- localStorage と同期
- 複雑な object でも deep で watch
Lv3: 発展 — VueUse を使う
問題
pnpm add @vueuse/core
<script setup>
import { useMouse, useLocalStorage, useDebounceFn, useClipboard } from "@vueuse/core";
const { x, y } = useMouse();
const theme = useLocalStorage("theme", "light");
const debounced = useDebounceFn(() => console.log("debounced!"), 500);
const { copy, copied } = useClipboard();
</script>
<template>
<p>Mouse: {{ x }}, {{ y }}</p>
<button @click="theme = theme === 'light' ? 'dark' : 'light'">{{ theme }}</button>
<input @input="debounced" />
<button @click="copy('Hello!')">Copy</button>
<span v-if="copied">Copied!</span>
</template>VueUse の便利 composable 4種を試す。
合格基準
- 4 composable とも動く
- 「自前で書く前に VueUse」を理解
提出
submission.md に3問の回答を。