# 銀行アプリを作成する Part 4: 状態管理の概念 ## 講義前クイズ [講義前クイズ](https://ff-quizzes.netlify.app/web/quiz/47) ## はじめに 状態管理は、ボイジャー宇宙船のナビゲーションシステムのようなものです。すべてがスムーズに動作しているときはほとんど気づきませんが、問題が発生すると、星間空間に到達するか、宇宙の虚空に迷子になるかの違いになります。ウェブ開発において、状態はアプリケーションが覚えておく必要があるすべてを表します。例えば、ユーザーのログイン状態、フォームデータ、ナビゲーション履歴、一時的なインターフェース状態などです。 あなたの銀行アプリが単純なログインフォームからより高度なアプリケーションへと進化するにつれて、いくつかの一般的な課題に直面しているかもしれません。ページをリフレッシュすると、ユーザーが予期せずログアウトされる。ブラウザを閉じるとすべての進捗が消える。問題をデバッグすると、異なる方法で同じデータを変更する複数の関数を探し回ることになる。 これらはコーディングが悪い兆候ではなく、アプリケーションがある程度の複雑さの閾値に達したときに自然に発生する成長痛です。すべての開発者が、アプリが「概念実証」から「本番準備完了」に移行する際にこれらの課題に直面します。 このレッスンでは、銀行アプリを信頼性の高いプロフェッショナルなアプリケーションに変える集中型状態管理システムを実装します。データフローを予測可能に管理し、ユーザーセッションを適切に保持し、現代のウェブアプリケーションが必要とするスムーズなユーザー体験を作り出す方法を学びます。 ## 前提条件 状態管理の概念に進む前に、開発環境が適切に設定されていることと、銀行アプリの基盤が整っていることが必要です。このレッスンは、このシリーズの以前の部分の概念とコードに直接基づいています。 以下のコンポーネントが準備できていることを確認してください: **必要なセットアップ:** - [データ取得レッスン](../3-data/README.md)を完了する - アプリが正常にアカウントデータを読み込み表示できること - [Node.js](https://nodejs.org) をシステムにインストールしてバックエンドAPIを実行する - アカウントデータ操作を処理するために[サーバーAPI](../api/README.md)をローカルで開始する **環境のテスト:** ターミナルで以下のコマンドを実行してAPIサーバーが正しく動作していることを確認してください: ```sh curl http://localhost:5000/api # -> should return "Bank API v1.0.0" as a result ``` **このコマンドが行うこと:** - **GETリクエスト**をローカルAPIサーバーに送信する - **接続をテスト**し、サーバーが応答していることを確認する - **APIバージョン情報**を返す(すべてが正常に動作している場合) --- ## 現在の状態問題の診断 シャーロック・ホームズが犯罪現場を調査するように、現在の実装で何が起きているのかを正確に理解する必要があります。そうすれば、消えるユーザーセッションの謎を解くことができます。 以下の簡単な実験を行い、状態管理の課題を明らかにしましょう: **🧪 この診断テストを試してみてください:** 1. 銀行アプリにログインし、ダッシュボードに移動する 2. ブラウザページをリフレッシュする 3. ログイン状態に何が起きるか観察する ログイン画面にリダイレクトされる場合、典型的な状態保持問題を発見したことになります。この動作は、現在の実装がページロードごとにリセットされるJavaScript変数にユーザーデータを保存しているために発生します。 **現在の実装の問題:** [前回のレッスン](../3-data/README.md)で使用した単純な`account`変数は、ユーザー体験とコードの保守性に影響を与える3つの重大な問題を引き起こします: | 問題 | 技術的原因 | ユーザーへの影響 | |------|------------|------------------| | **セッションの喪失** | ページリフレッシュでJavaScript変数がクリアされる | ユーザーは頻繁に再認証が必要 | | **更新の分散** | 複数の関数が状態を直接変更する | デバッグがますます困難になる | | **不完全なクリーンアップ** | ログアウト時にすべての状態参照がクリアされない | セキュリティとプライバシーの懸念 | **アーキテクチャの課題:** タイタニックの区画設計が複数の区画が同時に浸水したときに堅牢ではなくなったように、これらの問題を個別に修正しても、根本的なアーキテクチャの問題は解決されません。包括的な状態管理ソリューションが必要です。 > 💡 **ここで実際に達成しようとしていることは何ですか?** [状態管理](https://en.wikipedia.org/wiki/State_management)は、2つの基本的なパズルを解くことに関するものです: 1. **データはどこにあるのか?**: どの情報を持っていて、それがどこから来ているのかを追跡する 2. **全員が同じページにいるか?**: ユーザーが見るものが実際に起きていることと一致しているか確認する **私たちの計画:** 無駄な努力をする代わりに、**集中型状態管理**システムを作成します。これは、すべての重要なものを整理する非常に有能な人がいるようなものです: ![HTML、ユーザーアクション、状態間のデータフローを示すスキーマ](../../../../translated_images/data-flow.fa2354e0908fecc89b488010dedf4871418a992edffa17e73441d257add18da4.ja.png) **このデータフローの理解:** - **すべてのアプリケーション状態を一箇所に集中化**する - **すべての状態変更を制御された関数を通じてルーティング**する - **UIが現在の状態と同期し続けることを保証**する - **データ管理の明確で予測可能なパターンを提供**する > 💡 **プロフェッショナルな洞察**: このレッスンでは基本的な概念に焦点を当てています。複雑なアプリケーションでは、[Redux](https://redux.js.org)のようなライブラリがより高度な状態管理機能を提供します。これらの基本原則を理解することで、どの状態管理ライブラリでもマスターすることができます。 > ⚠️ **高度なトピック**: 状態変更によって自動的にトリガーされるUI更新については扱いません。これは[リアクティブプログラミング](https://en.wikipedia.org/wiki/Reactive_programming)の概念を含むためです。これを学習の次のステップとして検討してください! ### タスク: 状態構造の集中化 散在している状態管理を集中型システムに変えることから始めましょう。この最初のステップは、後に続くすべての改善の基盤を築きます。 **ステップ1: 集中型状態オブジェクトを作成する** 単純な`account`宣言を置き換えます: ```js let account = null; ``` 構造化された状態オブジェクトに変更します: ```js let state = { account: null }; ``` **この変更が重要な理由:** - **すべてのアプリケーションデータを一箇所に集中化**する - **後でさらに状態プロパティを追加する準備**をする - **状態と他の変数の間に明確な境界を作成**する - **アプリが成長するにつれてスケールするパターンを確立**する **ステップ2: 状態アクセスパターンを更新する** 新しい状態構造を使用するように関数を更新します: **`register()`と`login()`関数で**、以下を置き換えます: ```js account = ... ``` 以下に変更します: ```js state.account = ... ``` **`updateDashboard()`関数では**、冒頭に以下の行を追加します: ```js const account = state.account; ``` **これらの更新が達成すること:** - **既存の機能を維持しながら構造を改善**する - **より高度な状態管理の準備をする** - **状態データへのアクセスパターンを一貫性のあるものにする** - **集中型状態更新の基盤を確立する** > 💡 **注意**: このリファクタリングはすぐに問題を解決するわけではありませんが、強力な改善のための重要な基盤を作ります! ## 制御された状態更新の実装 状態を集中化したら、次のステップはデータ変更のための制御されたメカニズムを確立することです。このアプローチは予測可能な状態変更と簡単なデバッグを保証します。 その核心原則は航空管制に似ています。複数の関数が独立して状態を変更するのではなく、すべての変更を単一の制御された関数を通じて行います。このパターンは、データがいつどのように変更されるかを明確に監視します。 **不変状態管理:** `state`オブジェクトを[*不変*](https://en.wikipedia.org/wiki/Immutable_object)として扱います。つまり、直接変更することはありません。その代わり、各変更は更新されたデータを持つ新しい状態オブジェクトを作成します。 このアプローチは直接変更と比較して非効率に思えるかもしれませんが、デバッグ、テスト、アプリケーションの予測可能性を維持する上で大きな利点を提供します。 **不変状態管理の利点:** | 利点 | 説明 | 影響 | |------|------|------| | **予測可能性** | 変更は制御された関数を通じてのみ発生 | デバッグとテストが容易 | | **履歴追跡** | 各状態変更が新しいオブジェクトを作成 | 元に戻す/やり直し機能を可能にする | | **副作用防止** | 偶発的な変更がない | 謎のバグを防ぐ | | **パフォーマンス最適化** | 状態が実際に変更されたかどうかを簡単に検出 | 効率的なUI更新を可能にする | **JavaScriptの`Object.freeze()`による不変性:** JavaScriptは[`Object.freeze()`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze)を提供してオブジェクトの変更を防ぎます: ```js const immutableState = Object.freeze({ account: userData }); // Any attempt to modify immutableState will throw an error ``` **ここで何が起きているか:** - **直接のプロパティ割り当てや削除を防ぐ** - **変更試行時に例外をスローする** - **状態変更が制御された関数を通じて行われる必要があることを保証する** - **状態を更新する方法に明確な契約を作成する** > 💡 **深掘り**: [MDNドキュメント](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze#What_is_shallow_freeze)で*浅い*不変オブジェクトと*深い*不変オブジェクトの違いを学びましょう。この違いを理解することは複雑な状態構造にとって重要です。 ### タスク 新しい`updateState()`関数を作成しましょう: ```js function updateState(property, newData) { state = Object.freeze({ ...state, [property]: newData }); } ``` この関数では、新しい状態オブジェクトを作成し、[*スプレッド演算子 (`...`)*](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Operators/Spread_syntax#Spread_in_object_literals)を使用して前の状態からデータをコピーします。その後、[ブラケット記法](https://developer.mozilla.org/docs/Web/JavaScript/Guide/Working_with_Objects#Objects_and_properties) `[property]` を使用して状態オブジェクトの特定のプロパティを新しいデータで上書きします。最後に、`Object.freeze()`を使用してオブジェクトをロックし、変更を防ぎます。現在は`account`プロパティのみが状態に保存されていますが、このアプローチを使用すれば状態に必要なプロパティをいくらでも追加できます。 また、初期状態が凍結されるように`state`の初期化を更新します: ```js let state = Object.freeze({ account: null }); ``` その後、`register`関数を更新し、`state.account = result;`の代わりに以下を使用します: ```js updateState('account', result); ``` 同様に`login`関数も更新し、`state.account = data;`の代わりに以下を使用します: ```js updateState('account', data); ``` ここで、ユーザーが*ログアウト*をクリックした際にアカウントデータがクリアされない問題を修正する機会を得ます。 新しい`logout()`関数を作成します: ```js function logout() { updateState('account', null); navigate('/login'); } ``` `updateDashboard()`では、リダイレクト`return navigate('/login');`を`return logout();`に置き換えます。 新しいアカウントを登録し、ログアウトして再度ログインして、すべてが正しく動作することを確認してください。 > ヒント: `updateState()`の最後に`console.log(state)`を追加し、ブラウザの開発ツールのコンソールを開いてすべての状態変更を確認できます。 ## データ永続化の実装 前述のセッション喪失問題は、ブラウザセッションを超えてユーザー状態を保持する永続化ソリューションを必要とします。これにより、アプリケーションが一時的な体験から信頼性の高いプロフェッショナルなツールに変わります。 原子時計が停電中でも正確な時間を維持するために重要な状態を不揮発性メモリに保存するように、ウェブアプリケーションもブラウザセッションやページリフレッシュを超えて重要なユーザーデータを保持するための永続的なストレージメカニズムが必要です。 **データ永続化の戦略的質問:** 永続化を実装する前に、以下の重要な要素を検討してください: | 質問 | 銀行アプリの文脈 | 決定の影響 | |------|------------------|------------| | **データは機密性があるか?** | アカウント残高、取引履歴 | 安全なストレージ方法を選択する | | **どのくらいの期間保持するべきか?** | ログイン状態 vs 一時的なUI設定 | 適切な保存期間を選択する | | **サーバーが必要とするか?** | 認証トークン vs UI設定 | 共有要件を決定する | **ブラウザストレージオプション:** 現代のブラウザは、異なるユースケース向けに設計されたいくつかのストレージメカニズムを提供しています: **主要なストレージAPI:** 1. **[`localStorage`](https://developer.mozilla.org/docs/Web/API/Window/localStorage)**: 永続的な[キー/値ストレージ](https://en.wikipedia.org/wiki/Key%E2%80%93value_database) - **ブラウザセッションを超えてデータを保持**する - **ブラウザの再起動やコンピュータの再起動を生き延びる** - **特定のウェブサイトドメインにスコープされる** - **ユーザー設定やログイン状態に最適** 2. **[`sessionStorage`](https://developer.mozilla.org/docs/Web/API/Window/sessionStorage)**: 一時的なセッションストレージ - **localStorageと同様に動作**するが、アクティブなセッション中のみ - **ブラウザタブを閉じると自動的にクリアされる** - **一時的なデータに最適** 3. **[HTTPクッキー](https://developer.mozilla.org/docs/Web/HTTP/Cookies)**: サーバー共有ストレージ > 💡 **高度なオプション**: 大規模なデータセットを扱う複雑なオフラインアプリケーションの場合は、[`IndexedDB` API](https://developer.mozilla.org/docs/Web/API/IndexedDB_API)を検討してください。クライアント側で完全なデータベースを提供しますが、実装がより複雑になります。 ### タスク: localStorageの永続化を実装する ユーザーが明示的にログアウトするまでログイン状態を維持する永続的なストレージを実装しましょう。`localStorage`を使用して、ブラウザセッション間でアカウントデータを保存します。 **ステップ1: ストレージ設定を定義する** ```js const storageKey = 'savedAccount'; ``` **この定数が提供するもの:** - 保存データの一貫した識別子を**作成** - ストレージキー参照のタイプミスを**防止** - ストレージキーを簡単に変更可能に**する** - メンテナンス性の高いコードのベストプラクティスに**従う** **ステップ2: 自動永続化を追加する** `updateState()`関数の最後に以下の行を追加します: ```js localStorage.setItem(storageKey, JSON.stringify(state.account)); ``` **ここで何が起こるかの説明:** - アカウントオブジェクトをJSON文字列に**変換**して保存 - 一貫したストレージキーを使用してデータを**保存** - 状態変更が発生するたびに自動的に**実行** - 保存されたデータが常に現在の状態と同期されることを**保証** > 💡 **アーキテクチャの利点**: 状態更新をすべて`updateState()`に集中させたため、永続化を追加するのに1行のコードだけで済みました。これは良いアーキテクチャ設計の力を示しています! **ステップ3: アプリロード時に状態を復元する** 保存されたデータを復元する初期化関数を作成します: ```js function init() { const savedAccount = localStorage.getItem(storageKey); if (savedAccount) { updateState('account', JSON.parse(savedAccount)); } // Our previous initialization code window.onpopstate = () => updateRoute(); updateRoute(); } init(); ``` **初期化プロセスの理解:** - `localStorage`から以前に保存されたアカウントデータを**取得** - JSON文字列をJavaScriptオブジェクトに**解析** - 制御された更新関数を使用して状態を**更新** - ページロード時にユーザーセッションを自動的に**復元** - ルート更新の前に実行して状態が利用可能であることを**保証** **ステップ4: デフォルトルートを最適化する** 永続化を活用するためにデフォルトルートを更新します: `updateRoute()`内で以下を置き換えます: ```js // Replace: return navigate('/login'); return navigate('/dashboard'); ``` **この変更が理にかなっている理由:** - 新しい永続化システムを効果的に**活用** - ダッシュボードが認証チェックを**処理** - 保存されたセッションが存在しない場合にログインに自動的に**リダイレクト** - よりシームレスなユーザー体験を**作成** **実装のテスト:** 1. 銀行アプリにログインします 2. ブラウザページをリフレッシュします 3. ダッシュボードにログインしたままであることを確認します 4. ブラウザを閉じて再度開きます 5. アプリに戻り、ログイン状態が維持されていることを確認します 🎉 **達成感をゲット**: 永続的な状態管理を成功裏に実装しました!あなたのアプリはプロフェッショナルなウェブアプリのように動作します。 ## 永続性とデータの新鮮さのバランス 私たちの永続化システムはユーザーセッションを維持することに成功しましたが、新たな課題をもたらします。それはデータの古さです。複数のユーザーやアプリケーションが同じサーバーデータを変更すると、ローカルキャッシュ情報が古くなります。 この状況は、保存された星図と現在の天体観測の両方を頼りにしていたバイキングの航海士に似ています。星図は一貫性を提供しますが、航海士は変化する条件を考慮するために新しい観測が必要でした。同様に、私たちのアプリケーションは永続的なユーザー状態と最新のサーバーデータの両方が必要です。 **🧪 データの新鮮さ問題を発見する:** 1. `test`アカウントでダッシュボードにログインします 2. 別のソースからのトランザクションをシミュレートするために、ターミナルで以下のコマンドを実行します: ```sh curl --request POST \ --header "Content-Type: application/json" \ --data "{ \"date\": \"2020-07-24\", \"object\": \"Bought book\", \"amount\": -20 }" \ http://localhost:5000/api/accounts/test/transactions ``` 3. ブラウザでダッシュボードページをリフレッシュします 4. 新しいトランザクションが表示されるかどうかを確認します **このテストが示すもの:** - `localStorage`が「古い」(更新されていない)状態になることを**示す** - アプリ外でデータ変更が発生する現実的なシナリオを**シミュレート** - 永続性とデータの新鮮さの間の緊張を**明らかにする** **データの古さの課題:** | 問題 | 原因 | ユーザーへの影響 | |------|------|------------------| | **古いデータ** | `localStorage`が自動的に期限切れにならない | ユーザーが古い情報を見る | | **サーバー変更** | 他のアプリ/ユーザーが同じデータを変更 | プラットフォーム間での不一致 | | **キャッシュ vs 現実** | ローカルキャッシュがサーバー状態と一致しない | ユーザー体験の低下と混乱 | **解決策の戦略:** 永続性の利点を維持しながら、サーバーから新しいデータを自動的に取得する「ロード時のリフレッシュ」パターンを実装します。このアプローチはスムーズなユーザー体験を維持しながらデータの正確性を保証します。 ### タスク: データリフレッシュシステムを実装する 永続的な状態管理の利点を維持しながら、サーバーから新しいデータを自動的に取得するシステムを作成します。 **ステップ1: アカウントデータ更新機能を作成する** ```js async function updateAccountData() { const account = state.account; if (!account) { return logout(); } const data = await getAccount(account.user); if (data.error) { return logout(); } updateState('account', data); } ``` **この関数のロジックの理解:** - 現在ユーザーがログインしているかどうかを**確認**(state.accountが存在するか) - 有効なセッションが見つからない場合はログアウトに**リダイレクト** - 既存の`getAccount()`関数を使用してサーバーから新しいアカウントデータを**取得** - サーバーエラーを適切に処理し、無効なセッションをログアウトする - 制御された更新システムを使用して状態を**更新** - `updateState()`関数を通じて自動的に`localStorage`永続化を**トリガー** **ステップ2: ダッシュボードリフレッシュハンドラーを作成する** ```js async function refresh() { await updateAccountData(); updateDashboard(); } ``` **このリフレッシュ関数が達成すること:** - データリフレッシュとUI更新プロセスを**調整** - 新しいデータがロードされるまでUI更新を**待機** - ダッシュボードが最新情報を表示することを**保証** - データ管理とUI更新の間に明確な分離を**維持** **ステップ3: ルートシステムに統合する** ルート構成を更新してリフレッシュを自動的にトリガーします: ```js const routes = { '/login': { templateId: 'login' }, '/dashboard': { templateId: 'dashboard', init: refresh } }; ``` **この統合が機能する方法:** - ダッシュボードルートがロードされるたびにリフレッシュ関数を**実行** - ユーザーがダッシュボードに移動する際に常に最新データが表示されることを**保証** - 既存のルート構造を維持しながらデータの新鮮さを追加 - ルート固有の初期化の一貫したパターンを**提供** **データリフレッシュシステムのテスト:** 1. 銀行アプリにログインします 2. 先ほどのcurlコマンドを実行して新しいトランザクションを作成します 3. ダッシュボードページをリフレッシュするか、別のページに移動して戻ります 4. 新しいトランザクションが即座に表示されることを確認します 🎉 **完璧なバランスを達成**: あなたのアプリは永続的な状態のスムーズな体験と正確なサーバーデータを組み合わせることができました! ## GitHub Copilot Agentチャレンジ 🚀 Agentモードを使用して以下のチャレンジを完了してください: **説明:** 銀行アプリのための元に戻す/やり直し機能を備えた包括的な状態管理システムを実装してください。このチャレンジでは、状態履歴の追跡、不変の更新、ユーザーインターフェースの同期を含む高度な状態管理の概念を練習できます。 **プロンプト:** 以下を含む拡張状態管理システムを作成してください: 1) すべての以前の状態を追跡する状態履歴配列、2) 以前の状態に戻る元に戻す/やり直し機能、3) ダッシュボード上の元に戻す/やり直し操作用のUIボタン、4) メモリ問題を防ぐための最大履歴制限10状態、5) ユーザーがログアウトした際の履歴の適切なクリーンアップ。元に戻す/やり直し機能がアカウント残高の変更とブラウザリフレッシュ後も機能することを確認してください。 [agent modeについて詳しくはこちら](https://code.visualstudio.com/blogs/2025/02/24/introducing-copilot-agent-mode)。 ## 🚀 チャレンジ: ストレージの最適化 あなたの実装は現在、ユーザーセッション、データリフレッシュ、状態管理を効果的に処理しています。しかし、現在のアプローチがストレージ効率と機能性のバランスを最適に保っているかどうかを検討してください。 チェスマスターが重要な駒と交換可能なポーンを区別するように、効果的な状態管理には永続化すべきデータと常にサーバーから新鮮なデータを取得すべきデータを識別する必要があります。 **最適化分析:** 現在の`localStorage`実装を評価し、以下の戦略的な質問を検討してください: - ユーザー認証を維持するために必要な最小限の情報は何ですか? - ローカルキャッシュがほとんど利益をもたらさないほど頻繁に変更されるデータはどれですか? - ストレージの最適化がユーザー体験を損なうことなくパフォーマンスを向上させる方法は? この種のアーキテクチャ分析は、機能性と効率性の両方を考慮する経験豊富な開発者を際立たせます。 **実装戦略:** - 永続化が必要な重要なデータ(おそらくユーザー識別情報のみ)を**特定** - `localStorage`実装を変更して重要なセッションデータのみを保存する - ダッシュボード訪問時に常にサーバーから新しいデータを**ロード** - 最適化されたアプローチが同じユーザー体験を維持することを**テスト** **高度な考慮事項:** - 完全なアカウントデータを保存することと認証トークンのみを保存することのトレードオフを**比較** - 将来のチームメンバーのために決定と理由を**文書化** このチャレンジは、ユーザー体験とアプリケーション効率性の両方を考慮するプロフェッショナルな開発者のように考える手助けをします。さまざまなアプローチを試してみてください! ## 講義後のクイズ [講義後のクイズ](https://ff-quizzes.netlify.app/web/quiz/48) ## 課題 [「トランザクション追加」ダイアログを実装する](assignment.md) 以下は課題を完了した後の例です: ![「トランザクション追加」ダイアログの例のスクリーンショット](../../../../translated_images/dialog.93bba104afeb79f12f65ebf8f521c5d64e179c40b791c49c242cf15f7e7fab15.ja.png) --- **免責事項**: この文書はAI翻訳サービス[Co-op Translator](https://github.com/Azure/co-op-translator)を使用して翻訳されています。正確性を追求していますが、自動翻訳には誤りや不正確さが含まれる可能性があります。元の言語で記載された文書が正式な情報源とみなされるべきです。重要な情報については、専門の人間による翻訳を推奨します。この翻訳の使用に起因する誤解や誤解について、当社は責任を負いません。