0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

「Vue完全攻略への道」Day4-データ保持-StoreとBFF登録の棲み分け

Posted at

〇背景

SPAでは、画面遷移のたびに BFF(Backend for Frontend)へ保存・取得を繰り返すが、UXやネットワーク効率の観点から**フロント側での一時保持(Store)**が重要になる。

この記事では、**Store が担うべき“アプリ内状態の一時・共通管理”**と、**BFF が担う“正本データの登録・取得・整形”**の境界を、実装と運用の観点で整理する。


〇今回のコード例(Pinia + BFF想定)

// stores/useUserStore.ts
import { defineStore } from 'pinia'
import type { User } from '@/types'
import { fetchMe, updateProfile } from '@/api/bff' // BFF呼び出し

export const useUserStore = defineStore('user', {
  state: () => ({
    me: null as User | null,
    loading: false,
    error: null as string | null,
    // UIだけの状態(正本ではない)
    ui: { theme: 'light' as 'light' | 'dark' },
  }),
  actions: {
    async loadMe(force = false) {
      if (this.me && !force) return
      this.loading = true
      try {
        this.me = await fetchMe()
      } catch (e:any) {
        this.error = e?.message ?? 'failed to fetch'
      } finally {
        this.loading = false
      }
    },
    // 楽観的更新(Store→BFF)
    async saveProfile(patch: Partial<User>) {
      const prev = this.me
      this.me = { ...this.me!, ...patch } // 先に反映して体感速度UP
      try {
        this.me = await updateProfile(this.me!)
      } catch (e) {
        // 失敗時は巻き戻し
        this.me = prev
        throw e
      }
    },
  },
  getters: {
    isLoggedIn: (s) => !!s.me,
  },
})

// 画面側(例)
import { onMounted } from 'vue'
import { useUserStore } from '@/stores/useUserStore'

const user = useUserStore()
onMounted(() => user.loadMe()) // 取得済みならキャッシュをそのまま利用


〇調べる前の自分の認識

  • Store は未保存の入力や共通情報を保持する場所。
  • BFF へは保存・取得のトリガー時だけアクセスして、基本は Store を読み書きする。
  • ただし正のデータ源はサーバなので、Store は“キャッシュないし UI 状態”。

→ 概ね正しい。ただし「何を Store に置くか」「いつ破棄・再同期するか」の設計がカギ。


〇調べた結果

Store の立ち位置(役割の要約)

  • UI/アプリ状態の一時・共通管理(メモリ上)
  • 取得結果の短期キャッシュ(画面間で即時再利用)
  • 未保存ドラフトの保持(ウィザード/フォーム途中離脱の復帰)
  • ネットワーク遮断時の暫定表示(オフライン耐性の下地)

BFF の立ち位置(やるべきこと)

  • 正本データの登録・更新・取得(権限・バリデーション・集約)
  • クライアント向け整形(複数APIの集約、過不足のないスキーマ)
  • サーバサイドの永続化ポリシー(監査・トランザクション・整合性)

まとめると

  • Store=“速さ・一貫性・未保存の保持”
  • BFF=“正確さ・権限・永続性”

3. Store を使うとどう変わるか

Storeなし

画面A → BFF保存 → 画面遷移 → BFF取得 → 表示

Storeあり

画面A → Store更新(即時反映)+必要に応じBFF保存
画面遷移 → Storeから即時表示(必要時だけBFF再取得)


4. Storeを使うと便利なシーン

  • 検索条件・ページネーション・タブ状態を複数画面で共有
  • 一覧→詳細→戻る時にスクロール位置・フィルタを保持
  • ウィザード/マルチステップフォームのドラフト保持
  • ログインユーザ情報権限フラグを全体で使い回す
  • 楽観的UI(保存を待たずに先に描画)

5. 注意点(設計原則)

  1. Single Source of Truthの明確化
    • 正本はサーバ。Store は短期キャッシュ/一時状態
    • 「Storeが真」である期間・条件(フェッチ直後~TTL内など)を決める。
  2. 無尽蔵に詰め込まない
    • *UI専用(選択中ID・モーダル開閉)データキャッシュ(User, Items)**を分ける。
    • 大量リストはキー付きキャッシュページごとTTLで管理。
  3. 同期ポリシーを持つ
    • いつ再取得するか(ルート遷移時 / 明示リフレッシュ / TTL / フォーカス復帰時)。
    • 部分無効化(当該IDだけ再取得)を用意。
  4. 永続化は選択的に
    • pinia-plugin-persistedstate などで最低限を localStorage に。
    • センシティブ情報は永続化しない(トークンはHTTP-only Cookie推奨)。
  5. エラーとロールバック
    • 楽観的更新は巻き戻しを常に用意。
    • BFF検証エラーはフォームエラーへマップする責務をフロント側に。

〇動作解説(図解)

      ┌─────────┐        fetch/update        ┌──────────────┐
UI → │   Store   │  ───────────────────────▶ │      BFF      │
      └────┬────┘                             └──────┬───────┘
           │  (immediate read/write)                 │
           └───────────(response/refresh)◀──────────┘

- 基本は UI ↔ Store を同期、必要時のみ Store ↔ BFF。
- StoreはUIを滞留させて速く、BFFは正本として厳密に。


〇実務での注意点(チェックリスト)

設計

  • データ分類UI状態 / 一時ドラフト / 短期キャッシュ / 永続設定 を棚卸し
  • 同期戦略onRouteEnterload(force?)TTL手動更新
  • 無効化設計invalidate(id?) を用意(更新後の再取得は最小限)
  • エラー伝播:BFFエラー → Store → 画面(メッセージ規約)
  • PRG:保存後の画面遷移は router.replace で二重送信回避

実装

  • Storeの責務を薄く(I/Oは action、整形は getter、重ロジックは service 層へ)
  • 型の一元化types 共有。BFFレスポンスの型変換を関数化)
  • 並行要求の抑止loading/inflight フラグ or リクエストキーでデバウンス)
  • 権限・セッション失効(401/403時の一括ハンドリング)

テスト

  • キャッシュ命中でBFF未呼び出しになるか
  • TTL切れで自動再取得されるか
  • 楽観的更新失敗時に確実にロールバックするか
  • 直リンク/再読込で必要データを再構築できるか
  • 複数タブでの整合(永続化を使う場合のスタンプ比較)

〇まとめ・所感

  • Store=速さと一貫性、BFF=正確さと永続性。
  • UXを上げるカギは「Storeを一次キャッシュとして使い、必要な時だけBFF」に行くこと。
  • 一方で、“何をStoreに置くか/いつ無効化するか”が曖昧だとバグと同期ズレの温床になる。
  • 設計初期にデータ分類表同期ポリシーを決め、PRG・エラーマップ・無効化APIを整えると運用が安定する。

次回案(Day5):

  • キャッシュ無効化パターン徹底解説(ID単位/クエリ単位/TTL/イベント駆動)
  • pinia-plugin-persistedstate の安全な使い方(ホワイトリスト・暗号化・マイグレーション)
  • 楽観的UIパターン(Toasts、差分ハイライト、失敗時の再試行)

とりあえずStoreを利用することで部分的な送信受信が可能になり、通信・ソースコードにおける無駄を省けることに旨味があるということだ。そのアプリかサービス特有のキャッシュと考えると早い。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?