メインコンテンツにスキップ

Reduxの基礎、パート2:概念とデータフロー

Reduxの基礎、パート2:概念とデータフロー

学習内容
  • Reduxを使用するための重要な用語と概念
  • Reduxアプリ内でのデータの流れ方

はじめに

パート1:Reduxの概要では、Reduxとは何か、なぜそれを使用したいのか、そしてReduxコアで通常使用される他のReduxライブラリについて説明しました。また、Reduxアプリの動作例と、アプリを構成する要素についても見てきました。最後に、Reduxで使用される用語と概念についても簡単に触れました。

このセクションでは、これらの用語と概念について詳しく見ていき、Reduxアプリケーション内でのデータの流れ方について詳しく説明します。

注意

このチュートリアルでは、Reduxの背後にある原則と概念を説明するために、今日Reduxでアプリを構築する正しいアプローチとして教えているRedux Toolkitを使用した「モダンRedux」パターンよりも多くのコードを必要とする古いスタイルのReduxロジックパターンを意図的に示しています。これは、本番環境で使用できるプロジェクトを意味するものではありません

Redux Toolkitを使用した「モダン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は新しい状態に基づいて再レンダリングされます。

One-way data flow

ただし、特にコンポーネントがアプリケーションの異なる場所に配置されている場合、同じ状態を共有および使用する必要がある複数のコンポーネントがある場合、単純さが損なわれる可能性があります。場合によっては、親コンポーネントに「状態をリフトアップ」することで解決できますが、常に役立つとは限りません。

これを解決する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の用語

続行する前に、慣れておく必要のあるReduxの重要な用語がいくつかあります。

アクション

アクションは、typeフィールドを持つプレーンなJavaScriptオブジェクトです。アクションは、アプリケーションで発生した何かを記述するイベントと考えることができます

typeフィールドは、"todos/todoAdded"のような、このアクションを説明する名前を与える文字列である必要があります。通常、そのタイプ文字列は"ドメイン/イベント名"のように記述します。最初の部分は、このアクションが属する機能またはカテゴリであり、2番目の部分は発生した具体的なものです。

アクションオブジェクトには、何が起こったかに関する追加情報を持つ他のフィールドを含めることができます。慣例により、その情報をpayloadというフィールドに入れます。

典型的なアクションオブジェクトは次のようになります。

const addTodoAction = {
type: 'todos/todoAdded',
payload: 'Buy milk'
}

リデューサー

リデューサーは、現在の状態アクションオブジェクトを受け取り、必要に応じて状態を更新する方法を決定し、新しい状態を返す関数です:(状態、アクション) => 新しい状態リデューサーは、受信したアクション(イベント)タイプに基づいてイベントを処理するイベントリスナーと考えることができます。

情報

「リデューサー」関数は、Array.reduce()メソッドに渡すコールバック関数に似ているため、その名前が付けられています。

リデューサーは、常にいくつかの特定のルールに従う必要があります。

  • 状態アクション引数に基づいてのみ新しい状態値を計算する必要があります。
  • 既存の状態を変更することは許可されていません。代わりに、既存の状態をコピーし、コピーした値を変更することで、不変の更新を行う必要があります。
  • 非同期ロジックを実行したり、ランダムな値を計算したり、その他の「副作用」を引き起こしたりしてはなりません。

リデューサーのルール、それらがなぜ重要なのか、それらに正しく従う方法については後で詳しく説明します。

リデューサー関数内のロジックは、通常、同じ一連の手順に従います。

  • リデューサーがこのアクションに関心があるかどうかを確認します。
    • もしそうなら、状態のコピーを作成し、コピーを新しい値で更新して、それを返します。
  • それ以外の場合は、既存の状態を変更せずに返します。

各リデューサーが従うべき手順を示す、リデューサーの小さな例を次に示します。

const initialState = { value: 0 }

function counterReducer(state = initialState, action) {
// Check to see if the reducer cares about this action
if (action.type === 'counter/incremented') {
// 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/elseswitch、ループなど、あらゆる種類のロジックを内部で使用できます。

詳細な説明:なぜ「リデューサー」と呼ばれるのですか?

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

const actions = [
{ type: 'counter/incremented' },
{ type: 'counter/incremented' },
{ type: 'counter/incremented' }
]

const initialState = { value: 0 }

const finalResult = actions.reduce(counterReducer, initialState)
console.log(finalResult)
// {value: 3}

import { configureStore } from '@reduxjs/toolkit'

const store = configureStore({ reducer: counterReducer })

console.log(store.getState())
// {value: 0}

store.dispatch({ type: 'counter/incremented' })

console.log(store.getState())
// {value: 1}

const selectCounterValue = state => state.value

const currentValue = selectCounterValue(store.getState())
console.log(currentValue)
// 2

  • 状態は、特定の時点におけるアプリの状態を表します。
  • UIはその状態に基づいてレンダリングされます。
  • (ユーザーがボタンをクリックするなど)何かが発生すると、発生したことに基づいて状態が更新されます。
  • UIは新しい状態に基づいて再レンダリングされます。

Redux data flow diagram

    • UIは新しい状態に基づいて再レンダリングされます。