Upgrade to Pro — share decks privately, control downloads, hide ads and more …

コンポーネントのライフサイクルとグローバル状態の扱い方 #vuefes

Avatar for Ryutaro Yako Ryutaro Yako
October 23, 2026
28

コンポーネントのライフサイクルとグローバル状態の扱い方 #vuefes

2026年10月24日(土)に開催された「Vue Fes Japan 2026」での発表資料です。

セッションタイトル: コンポーネントのライフサイクルとグローバル状態の扱い方
https://vuefes.jp/2026/speaker/RyutaroYako

Avatar for Ryutaro Yako

Ryutaro Yako

October 23, 2026

Transcript

  1. 事業内容 当社はソーシャルメディアを利用した、ソーシャルメディア総合管理ツールや X・LINE・Instagram・TikTokなどSNSマーケティングツールを多数提供しています。 Belugaスタジオ Beluga キャンペーン その他 X/Facebook/Instagramなどのアカウ フォロー&リポストに代表されるXのイ SNSキャンペーンに連動した、生成AIを

    ントを一元管理できるソーシャルメディ ンスタントウィンをはじめとしたキャン 活用したシステムやプラットフォームが ア総合管理ツールです。企業のSNS運用 ペーンを複数展開しています。質の高い 提供する独自ツールやシステム開発も順 をサポートします。 キャンペーンの実施を支援しています。 次行っています。 Copyright ©Unique Vision Company. All Rights Reserved.
  2. アジェンダ 01. 発生したバグ 02. バグの原因 03. 応急処置 04. 根本的な修正 05.

    まとめ Copyright ©Unique Vision Company. All Rights Reserved.
  3. やりたかったこと: 入力値を localStorage に保存する ユーザーの入力値を localStorage に保存して、リロードしても維持されるようにしたい 保存した値をいろいろなコンポーネントから参照したい 保存には VueUse

    の useLocalStorage を使う localStorage と同期する ref を返し、値が変わると内部の watch が localStorage に 書き込む Copyright ©Unique Vision Company. All Rights Reserved.
  4. 実装: ref をキャッシュしてシングルトン化 題材: ライト/ダークのテーマ切り替え type Theme = "light" |

    "dark"; let theme: Ref<Theme> | undefined; export function useTheme() { if (!theme) { theme = useLocalStorage<Theme>("theme", "light"); } return theme; } いろいろなコンポーネントから呼ばれるので、ref を 1 つだけ作って使い回したかった 初回だけ useLocalStorage を呼び、作った ref をモジュールスコープの変数にキャッシュする 2 回目以降は、キャッシュした ref をそのまま返す ポイント: useLocalStorage が呼ばれるのは、最初の 1 回だけ Copyright ©Unique Vision Company. All Rights Reserved.
  5. 期待したこと: どこで切り替えても localStorage に保存される <!-- ページ A --> <script setup

    lang="ts"> const theme = useTheme(); // ← ここで初めて呼ばれる </script> <template> <select v-model="theme"> <option value="light">ライト</option> <option value="dark">ダーク</option> </select> </template> <!-- ページ B --> <script setup lang="ts"> const theme = useTheme(); // キャッシュ済みの ref を受け取る const setDark = () => { // localStorage に保存されるはず theme.value = "dark"; }; </script> どのコンポーネントからでも ref を共有できる どこで値を変えても localStorage に保存される Copyright ©Unique Vision Company. All Rights Reserved.
  6. 起こったこと: localStorage の値が変わらない 再現手順 ページ A を開く ページ B に移動してダークに切り替える

    ページ B での挙動: 切り替え前 切り替え後 ref の値 "light" localStorage の値 "light" theme.value = "dark" ref の値 "dark" localStorage の値 "light" 画面の表示(ref の値)は切り替わった localStorage に値が保存されなかった エラーは出ず画面も正しく見えるので、ずっと問題に気づけなかった Copyright ©Unique Vision Company. All Rights Reserved. 変わらない
  7. エフェクトとは リアクティブな値を監視し、変わったら関連す る処理を動かす仕組み watch / watchEffect / テンプレートの 描画など エフェクトは止めるまで値を監視し続ける

    const count = ref(0); // 値を監視してログ出力する const stop = watch(count, (value) => { console.log(value); }); count.value++; // → 1 が出力される 止めても ref の値は普通に読み書きできる stop(); // エフェクトを止める そのエフェクトによる監視と処理だけが止 count.value++; // → 何も出力されない(値は更新される) まる コンポーネント内で作成されたエフェクトは、 unmount されると自動的に止まる Copyright ©Unique Vision Company. All Rights Reserved.
  8. 起こっていたこと: ページ遷移によって localStorage への保存が止まっていた ページ A を開く ページ B へ移動する

    useTheme() を初めて呼ぶ localStorage に保存するための watch が作られる ページ A が unmount される watch も一緒に止まる ダークに切り替える ref の値は変わる localStorage には保存されない キャッシュされていたのは ref だけで、watch はページ A と一緒に止まってしまっていた 2 回目以降の呼び出しは ref を返すだけで、watch は作り直されない その結果…localStorage には保存されない Copyright ©Unique Vision Company. All Rights Reserved.
  9. 応急処置: App.vue で初期化する App.vue で useTheme() を呼ぶ <script setup lang="ts">

    useTheme(); // ここで作られた watch は、アプリを終了させるまで止まらない </script> useTheme() が各ページよりも先に呼ばれる watch は App.vue の中で作られる App.vue はアプリが終了するまで unmount されないので、watch も止まらなくなる この実装の問題点 「App.vue で先に呼ばないと壊れる」という暗黙のルールが生まれる もっとよい直し方はないか…? Copyright ©Unique Vision Company. All Rights Reserved.
  10. effectScope: エフェクトをまとめて破棄するための仕組み Vue はエフェクトを effectScope という単位 でまとめて管理している scope.run() : 中で作成されたエフェクトを登録する

    scope.stop() : 登録されたエフェクトをまとめて破棄する const scope = effectScope(); scope.run(() => { // エフェクト 1 watch(count, () => { /* ... */ }); // エフェクト 2 watchEffect(() => { /* ... */ }); }); // 2 つのエフェクトがまとめて破棄される scope.stop(); Copyright ©Unique Vision Company. All Rights Reserved.
  11. コンポーネントの内部でも effectScope が利用されている コンポーネントは、それぞれ自分の effectScope を持っている setup の中で作成された watch は、そのコンポーネントの

    effectScope に登録される unmount されると effectScope が停止し、登録された watch も一緒に破棄される effectScope(動作中) effectScope(停止) unmount const count = ref(0); watch(count, () => { /* ... */ }); scope.stop() const count = ref(0); watch(count, () => { /* ... */ }); ページ A の watch が止まったのも、App.vue に移動させると直ったのも、このため Copyright ©Unique Vision Company. All Rights Reserved.
  12. どのコンポーネントにも属さない effectScope effectScope の中で作られた effectScope は 親子関係になる 親が止まると子も一緒に止まる // どこにも属さない

    effectScope const detached = effectScope(true); effectScope(true) とすると、 どこにも属さない effectScope になる unmount の影響を受けず、エフェクトを動 かし続けることができる <script setup lang="ts"> // 子の effectScope const child = effectScope(); child.run(() => watch(a, onChangeA)); detached.run(() => watch(b, onChangeB)); // unmount 時の挙動: // - child の watch: 止まる // - detached の watch: 動き続ける </script> Copyright ©Unique Vision Company. All Rights Reserved.
  13. 根本的な修正: VueUse の createGlobalState を使う useLocalStorage と createGlobalState を組み合わせる //

    useTheme.ts type Theme = "light" | "dark"; export const useTheme = createGlobalState(() => { return useLocalStorage<Theme>("theme", "light"); }); 初回だけ初期化してキャッシュする点は、自前の実装と同じ 違いは、 createGlobalState では初期化が effectScope(true) の中で行われること そのため、localStorage に保存するための watch がどのコンポーネントにも属さなくなる! この実装の利点 どこで最初に呼んでも同じように動くので、呼び出す順番を気にしなくてよい Copyright ©Unique Vision Company. All Rights Reserved.