Redux Essentials, Part 1: Redux の概要と概念
- Reduxとは何か、そしてなぜそれを使いたいのか
- Reduxの重要な用語と概念
- Reduxアプリケーションにおけるデータの流れ方
はじめに
Redux Essentials チュートリアルへようこそ!このチュートリアルでは、Redux を紹介し、最新の推奨ツールとベストプラクティスを使用して、正しい方法で使用する方法を教えます。終了するまでに、ここで学んだツールとパターンを使用して、独自の Redux アプリケーションの構築を開始できるようになるはずです。
このチュートリアルのパート 1 では、Redux を使用するために知っておく必要のある重要な概念と用語について説明し、パート 2: Redux アプリケーション構造では、基本的な React + Redux アプリケーションを調べて、各要素がどのように組み合わされているかを確認します。
パート 3: 基本的な Redux データフローから、その知識を使用して、いくつかの実用的な機能を持つ小さなソーシャルメディアフィードアプリケーションを構築し、実際にどのように機能するかを確認し、Reduxを使用するためのいくつかの重要なパターンとガイドラインについて説明します。
このチュートリアルの読み方
このページでは、Reduxを正しい方法で使用する方法を示すことに焦点を当て、Reduxアプリケーションを正しく構築する方法を理解できるように、概念を十分に説明します。
これらの説明は初心者向けになるように努めていますが、すでに知っていることについていくつかの前提を立てる必要があります。
- HTML と CSSに精通していること。
- ES2015 の構文と機能に精通していること。
- React 用語の知識: JSX、State、関数コンポーネント、Props、および Hooks
- 非同期 JavaScriptとAJAX リクエストの作成に関する知識
これらのトピックにまだ慣れていない場合は、まずこれらのトピックに慣れてから、Redux について学ぶために戻ってくることをお勧めします。準備ができたら、お待ちしています!
ブラウザにReactとRedux DevTools拡張機能がインストールされていることを確認する必要があります。
- React DevTools 拡張機能
- Redux DevTools 拡張機能
Reduxとは?
そもそも、この「Redux」というものが何なのかを理解すると役立ちます。それは何をするのでしょうか?どのような問題を解決するのに役立つのでしょうか?なぜそれを使いたいのでしょうか?
Redux は、「アクション」と呼ばれるイベントを使用して、アプリケーションの状態を管理および更新するためのパターンとライブラリです。アプリケーション全体で使用する必要がある状態の一元化されたストアとして機能し、状態が予測可能な方法でのみ更新されるようにルールが設定されています。
なぜ Redux を使用する必要があるのでしょうか?
Redux は、「グローバル」状態、つまりアプリケーションの多くの部分で必要とされる状態を管理するのに役立ちます。
Redux が提供するパターンとツールにより、アプリケーションの状態がいつ、どこで、なぜ、どのように更新されているか、およびそれらの変更が発生した場合にアプリケーションロジックがどのように動作するかを理解しやすくなります。Redux は、予測可能でテスト可能なコードを作成するように導きます。これにより、アプリケーションが期待どおりに動作するという確信が得られます。
いつ Redux を使用する必要がありますか?
Redux は、共有状態の管理に対処するのに役立ちますが、他のツールと同様に、トレードオフがあります。学習する概念が増え、記述するコードも増えます。また、コードに間接的な要素が追加され、特定の制限に従う必要があります。これは、短期的な生産性と長期的な生産性の間のトレードオフです。
Redux は、次のような場合にさらに役立ちます。
- アプリの多くの場所で必要となる大量のアプリケーション状態がある
- アプリの状態が時間とともに頻繁に更新される
- その状態を更新するロジックが複雑になる可能性がある
- アプリのコードベースが中規模または大規模であり、多くの人が作業する可能性がある
すべてのアプリに Redux が必要なわけではありません。構築しているアプリの種類について時間をかけて考え、取り組んでいる問題を解決するのに最適なツールを決定してください。
Redux がアプリに適した選択肢かどうか不明な場合は、次のリソースでさらにガイダンスが得られます。
Redux ライブラリとツール
Redux は、小さなスタンドアロンの JS ライブラリです。ただし、一般的には他のいくつかのパッケージで使用されます。
React-Redux
Redux は、任意の UI フレームワークと統合でき、最も頻繁に React で使用されます。React-Reduxは、React コンポーネントが状態の一部を読み取ったり、アクションをディスパッチしてストアを更新したりすることで、Redux ストアと対話できるようにする公式パッケージです。
Redux Toolkit
Redux Toolkitは、Redux ロジックを記述するための推奨アプローチです。これには、Redux アプリケーションを構築するために不可欠であると思われるパッケージと関数が含まれています。Redux Toolkit は、推奨されるベストプラクティスを組み込み、ほとんどの Redux タスクを簡略化し、一般的なミスを防ぎ、Redux アプリケーションをより簡単に記述できるようにします。
Redux DevTools 拡張機能
Redux DevTools 拡張機能は、Redux ストアでの状態の変更の履歴を時系列で表示します。これにより、「タイムトラベルデバッグ」などの強力なテクニックを使用して、アプリケーションを効果的にデバッグできます。
Redux の用語と概念
実際のコードに入る前に、Redux を使用するために知っておく必要のある用語と概念について説明しましょう。
状態管理
まず、小さな React カウンターコンポーネントを見てみましょう。コンポーネントの状態の数値を追跡し、ボタンがクリックされると数値を増やします。
function Counter() {
// State: a counter value
const [counter, setCounter] = useState(0)
// Action: code that causes an update to the state when something happens
const increment = () => {
setCounter(prevCounter => prevCounter + 1)
}
// View: the UI definition
return (
<div>
Value: {counter} <button onClick={increment}>Increment</button>
</div>
)
}
これは、以下の部分からなる自己完結型のアプリです。
- ステート:アプリを動かす真実のソースです。
- ビュー:現在のステートに基づいたUIの宣言的な記述です。
- アクション:ユーザーの入力に基づいてアプリ内で発生するイベントで、ステートの更新をトリガーします。
これは、「一方向データフロー」の小さな例です。
- ステートは、特定の時点でのアプリの状態を表します。
- UIは、そのステートに基づいてレンダリングされます。
- 何か(ユーザーがボタンをクリックするなど)が発生すると、発生した内容に基づいてステートが更新されます。
- UIは、新しいステートに基づいて再レンダリングされます。
しかし、同じステートを共有し、使用する必要がある複数のコンポーネント、特にそれらのコンポーネントがアプリケーションの異なる場所に位置している場合、この単純さが損なわれる可能性があります。場合によっては、親コンポーネントへの「ステートのリフトアップ」によって解決できることもありますが、常に役立つとは限りません。
これを解決する1つの方法は、共有ステートをコンポーネントから抽出し、コンポーネントツリーの外側の集中化された場所に配置することです。これにより、コンポーネントツリーは大きな「ビュー」になり、ツリー内のどこにあっても、どのコンポーネントもステートにアクセスしたり、アクションをトリガーしたりできるようになります!
ステート管理に関わる概念を定義および分離し、ビューとステート間の独立性を維持するルールを適用することで、コードの構造と保守性が向上します。
これがReduxの基本的なアイデアです。アプリケーションのグローバルステートを格納するための一元化された単一の場所と、コードを予測可能にするためにそのステートを更新する際に従うべき特定のパターンです。
不変性
「可変」とは「変更可能」という意味です。「不変」なものは、決して変更できません。
JavaScriptのオブジェクトと配列は、デフォルトではすべて可変です。オブジェクトを作成した場合、そのフィールドの内容を変更できます。配列を作成した場合も、内容を変更できます。
const obj = { a: 1, b: 2 }
// still the same object outside, but the contents have changed
obj.b = 3
const arr = ['a', 'b']
// In the same way, we can change the contents of this array
arr.push('c')
arr[1] = 'd'
これは、オブジェクトまたは配列を変更すると呼ばれます。メモリ内の同じオブジェクトまたは配列の参照ですが、オブジェクト内の内容が変更されました。
値を不変に更新するためには、コードは既存のオブジェクト/配列のコピーを作成し、コピーを変更する必要があります。.
これは、JavaScriptの配列/オブジェクトのスプレッド演算子や、元の配列を変更するのではなく新しい配列のコピーを返す配列メソッドを使用して手動で行うことができます。
const obj = {
a: {
// To safely update obj.a.c, we have to copy each piece
c: 3
},
b: 2
}
const obj2 = {
// copy obj
...obj,
// overwrite a
a: {
// copy obj.a
...obj.a,
// overwrite c
c: 42
}
}
const arr = ['a', 'b']
// Create a new copy of arr, with "c" appended to the end
const arr2 = arr.concat('c')
// or, we can make a copy of the original array:
const arr3 = arr.slice()
// and mutate the copy:
arr3.push('c')
Reduxは、すべてのステートの更新が不変に行われることを期待しています。これの重要性とその方法については、後ほど詳しく説明します。また、不変の更新ロジックをより簡単に記述する方法についても説明します。
JavaScriptにおける不変性の仕組みの詳細については、以下を参照してください。
用語
先に進む前に、知っておく必要のあるReduxの重要な用語がいくつかあります。
アクション
アクションは、type
フィールドを持つプレーンなJavaScriptオブジェクトです。アクションは、アプリケーションで発生したことを記述するイベントと考えることができます。
type
フィールドは、"todos/todoAdded"
のように、このアクションを説明する名前を与える文字列である必要があります。通常、その型の文字列は"domain/eventName"
のように記述します。最初の部分は、このアクションが属する機能またはカテゴリであり、2番目の部分は、発生した特定のことです。
アクションオブジェクトには、何が起こったかに関する追加情報を含む他のフィールドを含めることができます。慣例として、その情報をpayload
と呼ばれるフィールドに入れます。
一般的なアクションオブジェクトは、次のようになります。
const addTodoAction = {
type: 'todos/todoAdded',
payload: 'Buy milk'
}
アクションクリエイター
アクションクリエイターは、アクションオブジェクトを作成して返す関数です。通常、毎回手動でアクションオブジェクトを記述する必要がないように、これを使用します。
const addTodo = text => {
return {
type: 'todos/todoAdded',
payload: text
}
}
リデューサー
リデューサーは、現在のstate
とaction
オブジェクトを受け取り、必要に応じてステートを更新する方法を決定し、新しいステートを返す関数です:(state, action) => newState
。リデューサーは、受信したアクション(イベント)タイプに基づいてイベントを処理するイベントリスナーと考えることができます。
「リデューサー」関数という名前は、Array.reduce()
メソッドに渡すコールバック関数に似ているためです。
リデューサーは常にいくつかの特定のルールに従う必要があります。
state
およびaction
引数に基づいて新しいステート値を計算するだけである必要があります。- 既存の
state
を変更することは許可されていません。代わりに、既存のstate
をコピーし、コピーした値を変更することで、不変の更新を行う必要があります。 - 非同期ロジックを実行したり、ランダムな値を計算したり、他の「副作用」を引き起こしたりしてはなりません。
後で、リデューサーのルールについて、その重要性や正しく従う方法などを含めて詳しく説明します。
リデューサー関数内のロジックは、通常、同じ一連の手順に従います。
- リデューサーがこのアクションに関心があるかどうかを確認します。
- 関心がある場合は、ステートのコピーを作成し、コピーを新しい値で更新して、それを返します。
- そうでない場合は、既存のステートを変更せずに返します。
以下に、各リデューサーが従うべき手順を示す、リデューサーの小さな例を示します。
const initialState = { value: 0 }
function counterReducer(state = initialState, action) {
// Check to see if the reducer cares about this action
if (action.type === 'counter/increment') {
// If so, make a copy of `state`
return {
...state,
// and update the copy with the new value
value: state.value + 1
}
}
// otherwise return the existing state unchanged
return state
}
リデューサーは、新しいステートを決定するために、if/else
、switch
、ループなど、あらゆる種類のロジックを使用できます。
詳細な説明:なぜ「リデューサー」と呼ばれるのですか?
Array.reduce()
メソッドを使用すると、値の配列を受け取り、配列内の各項目を一度に1つずつ処理し、単一の最終結果を返すことができます。これは、「配列を1つの値に縮小する」と考えることができます。
Array.reduce()
は、配列内の各項目に対して1回呼び出されるコールバック関数を引数として受け取ります。これは、次の2つの引数を取ります。
previousResult
:コールバックが前回返した値currentItem
:配列内の現在の項目
コールバックが最初に実行されるとき、previousResult
は使用できないため、最初のpreviousResult
として使用される初期値を渡す必要があります。
数値の配列を合計して合計を調べたい場合は、次のようなreduceコールバックを記述できます。
const numbers = [2, 5, 8]
const addNumbers = (previousResult, currentItem) => {
console.log({ previousResult, currentItem })
return previousResult + currentItem
}
const initialValue = 0
const total = numbers.reduce(addNumbers, initialValue)
// {previousResult: 0, currentItem: 2}
// {previousResult: 2, currentItem: 5}
// {previousResult: 7, currentItem: 8}
console.log(total)
// 15
このaddNumbers
「reduceコールバック」関数は、何も追跡する必要がないことに注意してください。previousResult
とcurrentItem
の引数を取り、それらを使用して何かを行い、新しい結果値を返します。
Reduxリデューサー関数は、この「reduceコールバック」関数とまったく同じアイデアです!これは、「前の結果」(state
)と「現在の項目」(action
オブジェクト)を取り、これらの引数に基づいて新しいステート値を決定し、その新しいステートを返します。
Reduxアクションの配列を作成し、reduce()
を呼び出し、リデューサー関数を渡すと、同じように最終結果が得られます。
const actions = [
{ type: 'counter/increment' },
{ type: 'counter/increment' },
{ type: 'counter/increment' }
]
const initialState = { value: 0 }
const finalResult = actions.reduce(counterReducer, initialState)
console.log(finalResult)
// {value: 3}
Reduxリデューサーは、アクションのセットを(時間をかけて)単一のステートに縮小すると言えます。違いは、Array.reduce()
ではすべてが一度に行われ、Reduxでは実行中のアプリのライフタイムにわたって行われるということです。
ストア
現在のReduxアプリケーションの状態は、ストアと呼ばれるオブジェクトに存在します。
ストアはリデューサーを渡すことによって作成され、現在のステート値を返すgetState
というメソッドを持っています。
import { configureStore } from '@reduxjs/toolkit'
const store = configureStore({ reducer: counterReducer })
console.log(store.getState())
// {value: 0}
ディスパッチ
Reduxストアには、dispatch
というメソッドがあります。ステートを更新する唯一の方法は、store.dispatch()
を呼び出し、アクションオブジェクトを渡すことです。ストアはリデューサー関数を実行し、新しいステート値を内部に保存し、getState()
を呼び出して更新された値を取得できます。
store.dispatch({ type: 'counter/increment' })
console.log(store.getState())
// {value: 1}
アクションのディスパッチは、アプリケーションで「イベントをトリガーする」と考えることができます。何かが起こり、ストアにそれを知らせたいと考えています。リデューサーはイベントリスナーのように機能し、関心のあるアクションを聞くと、それに応じてステートを更新します。
通常、適切なアクションをディスパッチするためにアクションクリエイターを呼び出します。
const increment = () => {
return {
type: 'counter/increment'
}
}
store.dispatch(increment())
console.log(store.getState())
// {value: 2}
セレクター
セレクターは、ストアの状態値から特定の情報を抽出する方法を知っている関数です。アプリケーションが大きくなるにつれて、アプリのさまざまな部分が同じデータを読み取る必要がある場合に、ロジックの繰り返しを回避するのに役立ちます。
const selectCounterValue = state => state.value
const currentValue = selectCounterValue(store.getState())
console.log(currentValue)
// 2
Reduxアプリケーションのデータフロー
以前、「一方向データフロー」について説明しましたが、これはアプリを更新するための一連の手順を示しています。
- ステートは、特定の時点でのアプリの状態を表します。
- UIは、そのステートに基づいてレンダリングされます。
- 何か(ユーザーがボタンをクリックするなど)が発生すると、発生した内容に基づいてステートが更新されます。
- UIは、新しいステートに基づいて再レンダリングされます。
特にReduxについては、これらの手順をさらに詳細に分解できます。
- 初期設定
- Reduxストアは、ルートリデューサー関数を使用して作成されます。
- ストアはルートリデューサーを一度呼び出し、戻り値を初期の
state
として保存します。 - UIが最初にレンダリングされると、UIコンポーネントはReduxストアの現在のステートにアクセスし、そのデータを使用してレンダリングするものを決定します。また、ステートが変更されたかどうかを知ることができるように、将来のストアの更新をサブスクライブします。
- 更新
- アプリでユーザーがボタンをクリックするなど、何かが発生します。
- アプリのコードは、
dispatch({type: 'counter/increment'})
のように、アクションをReduxストアにディスパッチします。 - ストアは、前の
state
と現在のaction
を使用してリデューサー関数を再度実行し、戻り値を新しいstate
として保存します。 - ストアは、サブスクライブしているUIのすべての部分に、ストアが更新されたことを通知します。
- ストアからデータを必要とする各UIコンポーネントは、必要なステートの部分が変更されたかどうかを確認します。
- データが変更されたことを検出した各コンポーネントは、新しいデータで再レンダリングを強制し、画面に表示される内容を更新できます。
データフローは視覚的に次のようになります。
学んだこと
Reduxには、覚えておくべき新しい用語と概念が多数あります。念のため、ここで説明した内容をまとめます。
- Reduxは、グローバルなアプリケーションの状態を管理するためのライブラリです。
- Reduxは通常、ReduxとReactを統合するためにReact-Reduxライブラリとともに使用されます。
- Redux Toolkitは、Reduxロジックを記述するための推奨される方法です。
- Reduxは、「一方向データフロー」アプリ構造を使用します。
- ステートは特定の時点でのアプリの状態を表し、UIはそのステートに基づいてレンダリングされます。
- アプリで何かが起こると
- UIはアクションをディスパッチします。
- ストアはリデューサーを実行し、発生した内容に基づいてステートが更新されます。
- ストアは、ステートが変更されたことをUIに通知します。
- UIは、新しいステートに基づいて再レンダリングされます。
- Reduxは、いくつかの種類のコードを使用します。
- アクションは、
type
フィールドを持つプレーンなオブジェクトであり、アプリで「何が起こったか」を記述します。 - リデューサーは、前のステート+アクションに基づいて新しいステート値を計算する関数です。
- Reduxストアは、アクションがディスパッチされるたびに、ルートリデューサーを実行します。
- アクションは、
次は?
ここまででReduxアプリの個々の構成要素を見てきました。次は、パート2:Redux Toolkit アプリケーション構造に進み、それぞれの要素がどのように組み合わさるかを示す、実際の動作例を見ていきましょう。