Pinia アクション設計 50 レシピ:リトライ・デバウンス・キャンセル・同時実行制御
Pinia を使った状態管理では、アクションの設計が重要です。特に、API 呼び出しや非同期処理を扱う場合、リトライ、デバウンス、キャンセル、同時実行制御などの高度なパターンが求められます。
本記事では、実務で役立つ Pinia アクション設計のレシピを 50 個厳選してご紹介します。初心者の方でも理解できるよう、具体的なコード例と共に段階的に解説していきますので、ぜひ最後までお読みください。
50 レシピ早見表
以下は、本記事で紹介する全 50 レシピの一覧です。目的に応じて最適なパターンを素早く見つけられます。
| # | レシピ名 | カテゴリ | 難易度 | 説明 | 主な用途 |
|---|---|---|---|---|---|
| 1 | 基本的なリトライ | リトライ制御 | ★☆☆ | 固定回数リトライ | ネットワークエラー対処 |
| 2 | 指数バックオフ | リトライ制御 | ★★☆ | 待機時間を徐々に増加 | サーバー過負荷時の再試行 |
| 3 | ジッターありリトライ | リトライ制御 | ★★☆ | ランダムな待機時間を追加 | リクエスト集中の分散 |
| 4 | 条件付きリトライ | リトライ制御 | ★★☆ | エラー種別で判断 | エラータイプ別処理 |
| 5 | タイムアウト付きリトライ | リトライ制御 | ★★★ | 制限時間内でリトライ | 長時間処理の制御 |
| 6 | 段階的リトライ | リトライ制御 | ★★☆ | 戦略を段階的に変更 | 複雑なリトライロジック |
| 7 | サーキットブレーカー | リトライ制御 | ★★★ | 連続失敗時に停止 | システム保護 |
| 8 | フォールバック付きリトライ | リトライ制御 | ★★☆ | 失敗時に代替処理 | 可用性向上 |
| 9 | リトライ状態の可視化 | リトライ制御 | ★★☆ | UI にリトライ情報を表示 | ユーザーフィードバック |
| 10 | カスタマイズ可能なリトライ | リトライ制御 | ★★★ | 設定で動作を変更 | 柔軟なリトライ制御 |
| 11 | 基本的なデバウンス | デバウンス・スロットル | ★☆☆ | 入力完了後に実行 | 検索ボックス最適化 |
| 12 | リーディングデバウンス | デバウンス・スロットル | ★★☆ | 最初の呼び出しを即座に実行 | 即時フィードバック |
| 13 | 基本的なスロットル | デバウンス・スロットル | ★☆☆ | 一定間隔で実行 | スクロールイベント制御 |
| 14 | アダプティブデバウンス | デバウンス・スロットル | ★★★ | 入力速度に応じて調整 | 動的な待機時間調整 |
| 15 | キャンセル可能なデバウンス | デバウンス・スロットル | ★★☆ | 手動でキャンセル可能 | ユーザー操作による中断 |
| 16 | プログレス表示付きデバウンス | デバウンス・スロットル | ★★☆ | 待機時間を可視化 | UX 向上 |
| 17 | 即時実行オプション | デバウンス・スロットル | ★★☆ | 条件により即座に実行 | 重要な入力の優先処理 |
| 18 | グループ化デバウンス | デバウンス・スロットル | ★★★ | 複数の入力をまとめて処理 | バッチ処理最適化 |
| 19 | 優先度付きスロットル | デバウンス・スロットル | ★★★ | 重要な処理を優先 | 優先順位制御 |
| 20 | メモリ効率的なデバウンス | デバウンス・スロットル | ★★☆ | メモリリークを防止 | 長期稼働アプリ |
| 21 | AbortController 基本 | キャンセル制御 | ★★☆ | fetch をキャンセル | HTTP リクエスト中断 |
| 22 | 自動キャンセル | キャンセル制御 | ★★☆ | 新しいリクエスト時に前回を中断 | 検索機能での重複防止 |
| 23 | タイムアウトキャンセル | キャンセル制御 | ★★☆ | 一定時間後に自動キャンセル | 長時間処理の制限 |
| 24 | 手動キャンセル | キャンセル制御 | ★☆☆ | ユーザー操作でキャンセル | キャンセルボタン実装 |
| 25 | コンポーネント連動キャンセル | キャンセル制御 | ★★★ | アンマウント時に自動キャンセル | メモリリーク防止 |
| 26 | 条件付きキャンセル | キャンセル制御 | ★★☆ | 状態変化でキャンセル | 動的なキャンセル制御 |
| 27 | グループキャンセル | キャンセル制御 | ★★★ | 複数のリクエストを一括キャンセル | 複数処理の一括中断 |
| 28 | 優先度付きキャンセル | キャンセル制御 | ★★★ | 低優先度の処理をキャンセル | リソース最適化 |
| 29 | リソースクリーンアップ | キャンセル制御 | ★★☆ | キャンセル時にリソース解放 | メモリ管理 |
| 30 | キャンセル理由の記録 | キャンセル制御 | ★★☆ | デバッグ用にログ記録 | トラブルシューティング |
| 31 | 重複実行防止 | 同時実行制御 | ★☆☆ | 実行中は再実行を防ぐ | ボタン二度押し防止 |
| 32 | リクエストキュー | 同時実行制御 | ★★★ | 順次実行するキュー | 順序保証が必要な処理 |
| 33 | Promise キャッシング | 同時実行制御 | ★★☆ | 同じリクエストを共有 | 重複リクエスト削減 |
| 34 | 並列度制限 | 同時実行制御 | ★★★ | 同時実行数を制限 | リソース制限 |
| 35 | 優先度キュー | 同時実行制御 | ★★★ | 優先順位に基づいて実行 | 重要度による処理順制御 |
| 36 | レートリミット | 同時実行制御 | ★★★ | 一定期間の実行回数を制限 | API 制限対応 |
| 37 | バッチ処理 | 同時実行制御 | ★★★ | 複数のリクエストをまとめる | ネットワーク効率化 |
| 38 | ストリーミング処理 | 同時実行制御 | ★★★ | データを分割して処理 | 大量データ処理 |
| 39 | リソースプール | 同時実行制御 | ★★★ | リソースを再利用 | コネクション管理 |
| 40 | デッドロック防止 | 同時実行制御 | ★★★ | 相互待機を回避 | 複雑な依存関係処理 |
| 41 | エラー分類 | エラーハンドリング | ★★☆ | エラーの種類を判別 | エラータイプ別処理 |
| 42 | グローバルエラーハンドラ | エラーハンドリング | ★★☆ | 共通エラー処理 | 一元的なエラー管理 |
| 43 | エラーリカバリー | エラーハンドリング | ★★★ | 自動的に回復 | 自動復旧処理 |
| 44 | ユーザーフレンドリーなメッセージ | エラーハンドリング | ★☆☆ | わかりやすいエラー表示 | UX 向上 |
| 45 | エラーログ送信 | エラーハンドリング | ★★☆ | サーバーへエラー報告 | エラー監視 |
| 46 | オフライン対応 | エラーハンドリング | ★★★ | オフライン時の処理 | オフラインファースト |
| 47 | 部分的エラー処理 | エラーハンドリング | ★★★ | 一部成功時の対処 | バッチ処理のエラー管理 |
| 48 | エラー境界 | エラーハンドリング | ★★☆ | エラーの影響範囲を限定 | 障害の局所化 |
| 49 | デバッグモード | エラーハンドリング | ★★☆ | 開発時の詳細情報表示 | 開発効率向上 |
| 50 | エラーメトリクス | エラーハンドリング | ★★★ | エラー率の監視 | パフォーマンス分析 |
この早見表を参考に、プロジェクトの要件に最適なレシピを選択してください。各レシピの詳細な実装方法は、後続のセクションで解説します。
背景
Pinia は Vue.js の公式状態管理ライブラリとして、Vuex の後継として開発されました。シンプルな API と TypeScript との親和性の高さから、多くのプロジェクトで採用されています。
Pinia のストアは、state(状態)、getters(算出プロパティ)、actions(アクション)の 3 つの要素で構成されます。このうち actions は、状態を変更するロジックや非同期処理を実装する場所として機能します。
以下の図は、Pinia ストアの基本構造を示しています。
mermaidflowchart TB
component["Vue コンポーネント"]
store["Pinia ストア"]
state["state<br/>(状態)"]
getters["getters<br/>(算出値)"]
actions["actions<br/>(処理)"]
api["外部 API"]
component -->|"dispatch"| actions
actions -->|"更新"| state
state -->|"参照"| getters
getters -->|"reactive"| component
actions -->|"HTTP 呼び出し"| api
api -->|"レスポンス"| actions
しかし、実際の開発では、単純な状態更新だけでは不十分なケースが多く存在します。ネットワークエラーへの対処、ユーザー入力の最適化、リソースの効率的な利用など、様々な課題に直面するでしょう。
Pinia アクションの基本形
typescriptimport { defineStore } from 'pinia';
export const useUserStore = defineStore('user', {
state: () => ({
users: [],
loading: false,
error: null,
}),
actions: {
async fetchUsers() {
this.loading = true;
try {
const response = await fetch('/api/users');
this.users = await response.json();
} catch (error) {
this.error = error;
} finally {
this.loading = false;
}
},
},
});
上記は最も基本的なアクションの形です。ローディング状態の管理とエラーハンドリングを含んでいますが、実務ではさらに高度な制御が必要になります。
課題
実際の開発現場では、以下のような課題に直面することがあります。
パフォーマンスとユーザー体験の課題
API 呼び出しが頻繁に発生すると、サーバーへの負荷が増大し、レスポンスが遅くなります。例えば、検索ボックスへの入力ごとに API を呼び出すと、1 文字入力するたびにリクエストが送信されてしまいます。
また、ネットワークが不安定な環境では、一時的なエラーでアクションが失敗することがあります。ユーザーに再実行を強いるのではなく、自動的にリトライする仕組みが求められるでしょう。
リソース管理の課題
同じアクションが複数回実行されると、重複したリクエストが発生します。例えば、ボタンの二度押しや、複数のコンポーネントから同時に同じデータを取得しようとする場合です。
さらに、ページ遷移などでコンポーネントがアンマウントされた後も、非同期処理が継続してしまうことがあります。これにより、不要な処理が実行され、メモリリークの原因となります。
以下の図は、適切な制御がない場合の問題を示しています。
mermaidsequenceDiagram
participant User as ユーザー
participant Component as コンポーネント
participant Action as アクション
participant API as API サーバー
User ->> Component: 検索入力 "a"
Component ->> Action: fetchData("a")
Action ->> API: リクエスト 1
User ->> Component: 検索入力 "ab"
Component ->> Action: fetchData("ab")
Action ->> API: リクエスト 2
User ->> Component: 検索入力 "abc"
Component ->> Action: fetchData("abc")
Action ->> API: リクエスト 3
Note over API: 3つのリクエストが<br/>並行実行される
API -->> Action: レスポンス 2
API -->> Action: レスポンス 3
API -->> Action: レスポンス 1 (遅延)
Note over Component: 古いデータで<br/>上書きされる可能性
エラーハンドリングの課題
エラーが発生した際、単にエラーメッセージを表示するだけでは不十分です。エラーの種類に応じて適切な処理が必要になります。
- ネットワークエラー: 自動リトライ
- 認証エラー: ログイン画面へリダイレクト
- バリデーションエラー: ユーザーへフィードバック
- サーバーエラー: エラーログの送信
これらの課題を解決するために、高度なアクション設計パターンが必要となるのです。
解決策
Pinia アクションの設計パターンを、目的別に分類して 50 個のレシピとしてご紹介します。これらのレシピを組み合わせることで、堅牢で効率的なアプリケーションを構築できるでしょう。
レシピ分類一覧
| # | カテゴリ | レシピ数 | 主な用途 |
|---|---|---|---|
| 1 | リトライ制御 | 10 | ネットワークエラーへの対処 |
| 2 | デバウンス・スロットル | 10 | パフォーマンス最適化 |
| 3 | キャンセル制御 | 10 | リソース管理 |
| 4 | 同時実行制御 | 10 | 重複リクエスト防止 |
| 5 | エラーハンドリング | 10 | 堅牢性の向上 |
カテゴリ 1: リトライ制御(レシピ 1-10)
リトライ制御は、一時的なエラーに対して自動的に再試行する仕組みです。ネットワークの不安定さやサーバーの一時的な過負荷に対処できます。
| # | レシピ名 | 難易度 | 説明 |
|---|---|---|---|
| 1 | 基本的なリトライ | ★☆☆ | 固定回数リトライ |
| 2 | 指数バックオフ | ★★☆ | 待機時間を徐々に増加 |
| 3 | ジッターありリトライ | ★★☆ | ランダムな待機時間を追加 |
| 4 | 条件付きリトライ | ★★☆ | エラー種別で判断 |
| 5 | タイムアウト付きリトライ | ★★★ | 制限時間内でリトライ |
| 6 | 段階的リトライ | ★★☆ | 戦略を段階的に変更 |
| 7 | サーキットブレーカー | ★★★ | 連続失敗時に停止 |
| 8 | フォールバック付きリトライ | ★★☆ | 失敗時に代替処理 |
| 9 | リトライ状態の可視化 | ★★☆ | UI にリトライ情報を表示 |
| 10 | カスタマイズ可能なリトライ | ★★★ | 設定で動作を変更 |
カテゴリ 2: デバウンス・スロットル(レシピ 11-20)
ユーザー入力やイベントの頻度を制御し、パフォーマンスを向上させるパターンです。
| # | レシピ名 | 難易度 | 説明 |
|---|---|---|---|
| 11 | 基本的なデバウンス | ★☆☆ | 入力完了後に実行 |
| 12 | リーディングデバウンス | ★★☆ | 最初の呼び出しを即座に実行 |
| 13 | 基本的なスロットル | ★☆☆ | 一定間隔で実行 |
| 14 | アダプティブデバウンス | ★★★ | 入力速度に応じて調整 |
| 15 | キャンセル可能なデバウンス | ★★☆ | 手動でキャンセル可能 |
| 16 | プログレス表示付きデバウンス | ★★☆ | 待機時間を可視化 |
| 17 | 即時実行オプション | ★★☆ | 条件により即座に実行 |
| 18 | グループ化デバウンス | ★★★ | 複数の入力をまとめて処理 |
| 19 | 優先度付きスロットル | ★★★ | 重要な処理を優先 |
| 20 | メモリ効率的なデバウンス | ★★☆ | メモリリークを防止 |
カテゴリ 3: キャンセル制御(レシピ 21-30)
実行中のアクションをキャンセルし、不要な処理を停止するパターンです。
| # | レシピ名 | 難易度 | 説明 |
|---|---|---|---|
| 21 | AbortController 基本 | ★★☆ | fetch をキャンセル |
| 22 | 自動キャンセル | ★★☆ | 新しいリクエスト時に前回をキャンセル |
| 23 | タイムアウトキャンセル | ★★☆ | 一定時間後に自動キャンセル |
| 24 | 手動キャンセル | ★☆☆ | ユーザー操作でキャンセル |
| 25 | コンポーネント連動キャンセル | ★★★ | アンマウント時に自動キャンセル |
| 26 | 条件付きキャンセル | ★★☆ | 状態変化でキャンセル |
| 27 | グループキャンセル | ★★★ | 複数のリクエストを一括キャンセル |
| 28 | 優先度付きキャンセル | ★★★ | 低優先度の処理をキャンセル |
| 29 | リソースクリーンアップ | ★★☆ | キャンセル時にリソース解放 |
| 30 | キャンセル理由の記録 | ★★☆ | デバッグ用にログ記録 |
カテゴリ 4: 同時実行制御(レシピ 31-40)
複数の同時実行を制御し、リソースを効率的に利用するパターンです。
| # | レシピ名 | 難易度 | 説明 |
|---|---|---|---|
| 31 | 重複実行防止 | ★☆☆ | 実行中は再実行を防ぐ |
| 32 | リクエストキュー | ★★★ | 順次実行するキュー |
| 33 | Promise キャッシング | ★★☆ | 同じリクエストを共有 |
| 34 | 並列度制限 | ★★★ | 同時実行数を制限 |
| 35 | 優先度キュー | ★★★ | 優先順位に基づいて実行 |
| 36 | レートリミット | ★★★ | 一定期間の実行回数を制限 |
| 37 | バッチ処理 | ★★★ | 複数のリクエストをまとめる |
| 38 | ストリーミング処理 | ★★★ | データを分割して処理 |
| 39 | リソースプール | ★★★ | リソースを再利用 |
| 40 | デッドロック防止 | ★★★ | 相互待機を回避 |
カテゴリ 5: エラーハンドリング(レシピ 41-50)
エラーを適切に処理し、ユーザー体験を向上させるパターンです。
| # | レシピ名 | 難易度 | 説明 |
|---|---|---|---|
| 41 | エラー分類 | ★★☆ | エラーの種類を判別 |
| 42 | グローバルエラーハンドラ | ★★☆ | 共通エラー処理 |
| 43 | エラーリカバリー | ★★★ | 自動的に回復 |
| 44 | ユーザーフレンドリーなメッセージ | ★☆☆ | わかりやすいエラー表示 |
| 45 | エラーログ送信 | ★★☆ | サーバーへエラー報告 |
| 46 | オフライン対応 | ★★★ | オフライン時の処理 |
| 47 | 部分的エラー処理 | ★★★ | 一部成功時の対処 |
| 48 | エラー境界 | ★★☆ | エラーの影響範囲を限定 |
| 49 | デバッグモード | ★★☆ | 開発時の詳細情報表示 |
| 50 | エラーメトリクス | ★★★ | エラー率の監視 |
これらのレシピを実際のコードで実装する方法を、次のセクションで詳しく見ていきましょう。
具体例
ここからは、主要なレシピの具体的な実装例をご紹介します。コードは機能ごとに分割し、理解しやすくしています。
レシピ 1: 基本的なリトライ
最も基本的なリトライ機能の実装です。指定した回数だけ処理を再試行します。
ユーティリティ関数の作成
まず、汎用的なリトライ関数を作成しましょう。
typescript// utils/retry.ts
/**
* 指定回数リトライする関数
* @param fn 実行する関数
* @param maxRetries 最大リトライ回数
* @param delay リトライ間隔(ミリ秒)
*/
export async function retry<T>(
fn: () => Promise<T>,
maxRetries: number = 3,
delay: number = 1000
): Promise<T> {
let lastError: Error;
for (let i = 0; i <= maxRetries; i++) {
try {
// 処理を実行
return await fn();
} catch (error) {
lastError = error as Error;
// 最後のリトライ以外は待機
if (i < maxRetries) {
await new Promise((resolve) =>
setTimeout(resolve, delay)
);
}
}
}
// すべて失敗した場合はエラーをスロー
throw lastError!;
}
ストアでの利用
作成したリトライ関数を Pinia ストアで使用します。
typescript// stores/user.ts
import { defineStore } from 'pinia';
import { retry } from '@/utils/retry';
export const useUserStore = defineStore('user', {
state: () => ({
users: [],
loading: false,
error: null,
retryCount: 0,
}),
actions: {
async fetchUsers() {
this.loading = true;
this.error = null;
this.retryCount = 0;
try {
// リトライ機能付きでデータ取得
const result = await retry(
async () => {
this.retryCount++;
const response = await fetch('/api/users');
if (!response.ok) {
throw new Error(
`HTTP Error: ${response.status}`
);
}
return response.json();
},
3, // 最大3回リトライ
1000 // 1秒待機
);
this.users = result;
} catch (error) {
this.error = error.message;
} finally {
this.loading = false;
}
},
},
});
上記のコードでは、retry 関数を使ってデータ取得処理を最大 3 回リトライします。各リトライの間には 1 秒の待機時間が設けられ、ネットワークの一時的な問題に対処できます。
レシピ 2: 指数バックオフ
リトライ回数が増えるごとに待機時間を長くする、より洗練された方法です。サーバーへの負荷を軽減できます。
typescript// utils/retry.ts
/**
* 指数バックオフでリトライする関数
* 待機時間: baseDelay * (2 ^ リトライ回数)
*/
export async function retryWithExponentialBackoff<T>(
fn: () => Promise<T>,
maxRetries: number = 5,
baseDelay: number = 1000
): Promise<T> {
let lastError: Error;
for (let i = 0; i <= maxRetries; i++) {
try {
return await fn();
} catch (error) {
lastError = error as Error;
if (i < maxRetries) {
// 指数的に増加する待機時間を計算
const waitTime = baseDelay * Math.pow(2, i);
console.log(
`リトライ ${
i + 1
}/${maxRetries}: ${waitTime}ms 待機`
);
await new Promise((resolve) =>
setTimeout(resolve, waitTime)
);
}
}
}
throw lastError!;
}
このパターンでは、1 回目は 1 秒、2 回目は 2 秒、3 回目は 4 秒というように待機時間が増えていきます。サーバーが過負荷の場合に特に有効でしょう。
レシピ 11: 基本的なデバウンス
ユーザーの入力が止まってから処理を実行するパターンです。検索ボックスなどで頻繁に使われます。
デバウンス関数の実装
typescript// utils/debounce.ts
/**
* デバウンス関数
* 指定時間内に再度呼ばれた場合、タイマーをリセット
*/
export function debounce<T extends (...args: any[]) => any>(
fn: T,
delay: number = 300
): (...args: Parameters<T>) => void {
let timeoutId: ReturnType<typeof setTimeout> | null =
null;
return function (...args: Parameters<T>) {
// 既存のタイマーをクリア
if (timeoutId) {
clearTimeout(timeoutId);
}
// 新しいタイマーをセット
timeoutId = setTimeout(() => {
fn(...args);
}, delay);
};
}
検索機能での利用例
typescript// stores/search.ts
import { defineStore } from 'pinia';
import { debounce } from '@/utils/debounce';
export const useSearchStore = defineStore('search', {
state: () => ({
query: '',
results: [],
loading: false,
}),
actions: {
// デバウンスされた検索アクション
debouncedSearch: null as any,
initDebouncedSearch() {
// 300ms の待機時間でデバウンス
this.debouncedSearch = debounce(
(query: string) => this.performSearch(query),
300
);
},
async performSearch(query: string) {
this.loading = true;
try {
const response = await fetch(
`/api/search?q=${query}`
);
this.results = await response.json();
} finally {
this.loading = false;
}
},
setQuery(query: string) {
this.query = query;
// デバウンスされた検索を実行
this.debouncedSearch(query);
},
},
});
コンポーネントからは、setQuery メソッドを呼ぶだけで、自動的にデバウンスされた検索が実行されます。
typescript// components/SearchBox.vue
<script setup lang="ts">
import { useSearchStore } from '@/stores/search'
const searchStore = useSearchStore()
searchStore.initDebouncedSearch()
function handleInput(event: Event) {
const query = (event.target as HTMLInputElement).value
// 入力のたびに呼ばれるが、実際の検索は300ms後
searchStore.setQuery(query)
}
</script>
<template>
<input
type="text"
:value="searchStore.query"
@input="handleInput"
placeholder="検索..."
/>
</template>
レシピ 21: AbortController でキャンセル
実行中のリクエストをキャンセルする機能です。ページ遷移時や新しい検索を開始した際に、前回のリクエストを中断できます。
キャンセル可能なアクションの実装
typescript// stores/data.ts
import { defineStore } from 'pinia';
export const useDataStore = defineStore('data', {
state: () => ({
data: null,
loading: false,
error: null,
// AbortController を保持
abortController: null as AbortController | null,
}),
actions: {
async fetchData(params: string) {
// 前回のリクエストをキャンセル
if (this.abortController) {
this.abortController.abort();
}
// 新しい AbortController を作成
this.abortController = new AbortController();
this.loading = true;
this.error = null;
try {
const response = await fetch(
`/api/data?params=${params}`,
{
// signal を渡してキャンセル可能に
signal: this.abortController.signal,
}
);
this.data = await response.json();
} catch (error) {
// キャンセルによるエラーは無視
if (error.name === 'AbortError') {
console.log('リクエストがキャンセルされました');
return;
}
this.error = error.message;
} finally {
this.loading = false;
this.abortController = null;
}
},
// 手動でキャンセルするメソッド
cancelFetch() {
if (this.abortController) {
this.abortController.abort();
this.abortController = null;
this.loading = false;
}
},
},
});
この実装により、新しいリクエストが開始されると前回のリクエストが自動的にキャンセルされます。ユーザーが「キャンセル」ボタンを押した場合にも対応できるでしょう。
レシピ 31: 重複実行防止
同じアクションが同時に複数回実行されるのを防ぐパターンです。ボタンの二度押し防止などに使用します。
typescript// stores/form.ts
import { defineStore } from 'pinia';
export const useFormStore = defineStore('form', {
state: () => ({
submitting: false,
result: null,
error: null,
}),
actions: {
async submitForm(formData: any) {
// すでに実行中の場合は何もしない
if (this.submitting) {
console.warn('フォーム送信が既に実行中です');
return;
}
this.submitting = true;
this.error = null;
try {
const response = await fetch('/api/submit', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(formData),
});
this.result = await response.json();
} catch (error) {
this.error = error.message;
} finally {
this.submitting = false;
}
},
},
});
ボタン側でも submitting 状態を使って二重送信を防げます。
typescript// components/SubmitButton.vue
<script setup lang="ts">
import { useFormStore } from '@/stores/form'
const formStore = useFormStore()
function handleSubmit() {
formStore.submitForm({ /* データ */ })
}
</script>
<template>
<button
@click="handleSubmit"
:disabled="formStore.submitting"
>
{{ formStore.submitting ? '送信中...' : '送信' }}
</button>
</template>
レシピ 33: Promise キャッシング
同じリクエストが複数の場所から呼ばれた場合、1 つのリクエストを共有するパターンです。リソースの無駄を削減できます。
Promise キャッシュの実装
typescript// stores/cache.ts
import { defineStore } from 'pinia';
export const useCacheStore = defineStore('cache', {
state: () => ({
users: null,
loading: false,
// 実行中の Promise を保持
pendingPromise: null as Promise<any> | null,
}),
actions: {
async fetchUsers() {
// すでに実行中の Promise がある場合はそれを返す
if (this.pendingPromise) {
console.log('既存のリクエストを再利用します');
return this.pendingPromise;
}
this.loading = true;
// 新しい Promise を作成して保存
this.pendingPromise = fetch('/api/users')
.then((response) => response.json())
.then((data) => {
this.users = data;
return data;
})
.finally(() => {
// 完了後はクリア
this.pendingPromise = null;
this.loading = false;
});
return this.pendingPromise;
},
},
});
複数のコンポーネントから同時に fetchUsers を呼んでも、実際の HTTP リクエストは 1 回だけになります。
レシピ 41: エラー分類
エラーの種類を判別し、適切な処理を行うパターンです。
エラー分類ユーティリティ
typescript// utils/error.ts
/**
* エラーの種類を判別
*/
export enum ErrorType {
NETWORK = 'network',
AUTH = 'auth',
VALIDATION = 'validation',
SERVER = 'server',
UNKNOWN = 'unknown',
}
export function classifyError(error: any): ErrorType {
// ネットワークエラー
if (error.name === 'NetworkError' || !navigator.onLine) {
return ErrorType.NETWORK;
}
// HTTP ステータスコードで判別
if (error.response) {
const status = error.response.status;
if (status === 401 || status === 403) {
return ErrorType.AUTH;
}
if (status >= 400 && status < 500) {
return ErrorType.VALIDATION;
}
if (status >= 500) {
return ErrorType.SERVER;
}
}
return ErrorType.UNKNOWN;
}
エラー種別ごとの処理
typescript// stores/api.ts
import { defineStore } from 'pinia';
import { classifyError, ErrorType } from '@/utils/error';
export const useApiStore = defineStore('api', {
state: () => ({
data: null,
error: null,
errorType: null as ErrorType | null,
}),
actions: {
async fetchData() {
try {
const response = await fetch('/api/data');
this.data = await response.json();
} catch (error) {
this.errorType = classifyError(error);
// エラー種別ごとに処理を分岐
switch (this.errorType) {
case ErrorType.NETWORK:
this.error = 'ネットワークに接続できません';
// オフライン処理やリトライ
break;
case ErrorType.AUTH:
this.error = '認証が必要です';
// ログイン画面へリダイレクト
this.redirectToLogin();
break;
case ErrorType.VALIDATION:
this.error = '入力内容を確認してください';
// バリデーションエラーの表示
break;
case ErrorType.SERVER:
this.error = 'サーバーエラーが発生しました';
// エラーログの送信
this.sendErrorLog(error);
break;
default:
this.error = '予期しないエラーが発生しました';
}
}
},
redirectToLogin() {
// ログイン画面への遷移処理
window.location.href = '/login';
},
sendErrorLog(error: any) {
// エラーログをサーバーに送信
fetch('/api/log-error', {
method: 'POST',
body: JSON.stringify({
error: error.message,
stack: error.stack,
timestamp: new Date().toISOString(),
}),
}).catch(() => {
// ログ送信失敗は無視
});
},
},
});
このように、エラーの種類に応じて適切な処理を実行することで、ユーザー体験が大きく向上します。
複合パターン: リトライ + デバウンス + キャンセル
実際のプロジェクトでは、複数のパターンを組み合わせることが多いでしょう。以下は検索機能の完全な実装例です。
typescript// stores/advancedSearch.ts
import { defineStore } from 'pinia';
import { debounce } from '@/utils/debounce';
import { retryWithExponentialBackoff } from '@/utils/retry';
export const useAdvancedSearchStore = defineStore(
'advancedSearch',
{
state: () => ({
query: '',
results: [],
loading: false,
error: null,
abortController: null as AbortController | null,
}),
actions: {
debouncedSearch: null as any,
initSearch() {
// デバウンス付き検索(500ms)
this.debouncedSearch = debounce(
(query: string) => this.search(query),
500
);
},
async search(query: string) {
// 前回のリクエストをキャンセル
if (this.abortController) {
this.abortController.abort();
}
// 空文字列の場合は検索しない
if (!query.trim()) {
this.results = [];
return;
}
this.abortController = new AbortController();
this.loading = true;
this.error = null;
try {
// リトライ機能付きで検索実行
const results = await retryWithExponentialBackoff(
async () => {
const response = await fetch(
`/api/search?q=${encodeURIComponent(
query
)}`,
{ signal: this.abortController!.signal }
);
if (!response.ok) {
throw new Error(
`検索エラー: ${response.status}`
);
}
return response.json();
},
3, // 最大3回リトライ
1000 // 初期待機時間1秒
);
this.results = results;
} catch (error) {
if (error.name === 'AbortError') {
// キャンセルは正常な動作
return;
}
this.error = error.message;
this.results = [];
} finally {
this.loading = false;
this.abortController = null;
}
},
setQuery(query: string) {
this.query = query;
this.debouncedSearch(query);
},
clearSearch() {
this.query = '';
this.results = [];
this.error = null;
if (this.abortController) {
this.abortController.abort();
}
},
},
}
);
この実装では、以下の機能が統合されています:
- デバウンス: ユーザーの入力が止まってから検索
- キャンセル: 新しい検索開始時に前回のリクエストをキャンセル
- リトライ: ネットワークエラー時に自動リトライ
- エラーハンドリング: 適切なエラー処理
以下の図は、これらの機能がどのように連携するかを示しています。
mermaidstateDiagram-v2
[*] --> Idle: 初期状態
Idle --> Debouncing: ユーザー入力
Debouncing --> Debouncing: 追加入力(タイマーリセット)
Debouncing --> Executing: 待機時間経過
Executing --> Retrying: エラー発生
Retrying --> Retrying: リトライ継続
Retrying --> Success: 成功
Retrying --> Failed: リトライ上限
Executing --> Success: 成功
Success --> Idle: 結果表示
Failed --> Idle: エラー表示
Executing --> Cancelled: 新しい入力
Retrying --> Cancelled: 新しい入力
Cancelled --> Debouncing: 次の検索へ
この図から、各状態がどのように遷移するかが理解できるでしょう。
まとめ
本記事では、Pinia アクション設計の 50 のレシピをご紹介しました。これらのパターンを活用することで、より堅牢で効率的なアプリケーションを構築できます。
特に重要なポイントをまとめます。
リトライ制御では、ネットワークの一時的な問題に対処できます。指数バックオフを使うことで、サーバーへの負荷も軽減できるでしょう。
デバウンスとスロットルは、ユーザー入力の最適化に不可欠です。検索ボックスやフォーム入力で積極的に活用してください。
キャンセル制御により、不要な処理を停止し、リソースを効率的に利用できます。AbortController は現代の Web 開発における標準的な手法です。
同時実行制御は、重複リクエストを防ぎ、アプリケーションのパフォーマンスを向上させます。Promise キャッシングなどの高度なテクニックも検討してみてください。
エラーハンドリングは、ユーザー体験を大きく左右します。エラーの種類に応じた適切な処理を実装することで、より親切なアプリケーションになるでしょう。
これらのレシピは、単独でも効果的ですが、組み合わせることでさらに強力になります。プロジェクトの要件に応じて、最適なパターンを選択してください。
実装する際は、まず基本的なパターンから始め、必要に応じて高度な機能を追加していくことをお勧めします。コードの可読性と保守性を保ちながら、段階的に改善していきましょう。
関連リンク
articlePinia アクション設計 50 レシピ:リトライ・デバウンス・キャンセル・同時実行制御
articlePinia × VueUse × Vite 雛形:型安全ストアとユーティリティを最短で組む
articlePinia と VueUse の useStorage/useFetch 比較:軽量レシピで代替できる境界
articlePinia ストア間の循環参照を断つ:依存分解とイベント駆動の現場テク
articlePinia 2025 アップデート総まとめ:非互換ポイントと安全な移行チェックリスト
articlePinia 可観測性の作り方:DevTools × OpenTelemetry で変更を可視化する
articleTauri vs Electron vs Flutter デスクトップ:UX・DX・配布のしやすさ徹底比較
articleRuby と Python を徹底比較:スクリプト・Web・データ処理での得意分野
articleshadcn/ui のバンドルサイズ影響を検証:Tree Shaking・Code Split の実測データ
articleRedis Docker Compose 構築:永続化・監視・TLS まで 1 ファイルで
articleRemix を選ぶ基準:認証・API・CMS 観点での要件適合チェック
articleReact で管理画面を最短構築:テーブル・フィルタ・権限制御の実例
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来