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

Redux エッセンシャル Part 1: Redux の概要と基本概念

非公式ベータ版翻訳

このページは PageTurner AI で翻訳されました(ベータ版)。プロジェクト公式の承認はありません。 エラーを見つけましたか? 問題を報告 →

学習内容
  • Reduxとは何か、なぜ使用するのか
  • Reduxの主要な用語と概念
  • Reduxアプリケーションにおけるデータフロー

はじめに

Redux エッセンシャルチュートリアルへようこそ!このチュートリアルでは、最新の推奨ツールとベストプラクティスを用いて、Reduxを正しい方法で導入し活用する方法を学びます。終了時点までに、ここで学んだツールとパターンを使って独自のReduxアプリケーションを構築できるようになるでしょう。

Part 1では、Reduxを使用するために必要な基本概念と用語を解説します。Part 2: Reduxアプリの構造では、典型的なReact + Reduxアプリケーションを分析し、各要素がどのように連携するかを確認します。

Part 3: Reduxの基本データフローからは、実用的な機能を備えた小さなSNSフィードアプリを構築します。実践で各要素がどのように機能するかを観察し、Reduxを使用する際の重要なパターンとガイドラインについても議論します。

このチュートリアルの読み方

このチュートリアルでは、Reduxを_正しい方法で_使用する方法を実践的に示すことに焦点を当て、概念を段階的に説明することで、正しいReduxアプリの構築方法を理解できるようにします。

解説は初心者にも理解しやすいよう心がけていますが、前提知識について以下の想定をしています:

これらのトピックに不安がある場合は、まずそれらを理解する時間を取ってから、Reduxの学習に戻ってくることをお勧めします。準備が整うまで、いつでもお待ちしています!

また、ブラウザにReact DevToolsとRedux DevToolsの拡張機能がインストールされていることを確認してください:

Reduxとは?

まず「Redux」とは何かを理解しましょう。何をするツールなのか?どんな問題解決に役立つのか?なぜ使いたいと思うのか?

Reduxはグローバルなアプリケーション状態を管理・更新するためのパターンおよびライブラリです。UIが「アクション」と呼ばれるイベントをトリガーして発生した事象を記述し、「リデューサ」と呼ばれる更新ロジックがそれに応じて状態を更新します。アプリケーション全体で共有される状態を一元管理するストアとして機能し、状態が予測可能な方法でのみ更新されることを保証するルールを備えています。

Reduxを使うべき理由

Reduxは「グローバル」な状態、つまりアプリケーションの多くの部分で必要とされる状態の管理を支援します。

Reduxが提供するパターンとツールは、アプリケーションの状態がいつ、どこで、なぜ、どのように更新されるのか、またその変化が発生した際にアプリケーションロジックがどう振る舞うのかを理解しやすくします。Reduxは予測可能でテスト可能なコード作成を導くため、アプリケーションが期待通りに動作するという確信を得るのに役立ちます。

Reduxを使うべきタイミング

Reduxは共有状態管理を支援しますが、あらゆるツールと同様にトレードオフがあります。学習すべき概念が増え、記述するコードも増加します。またコードに間接性が加わり、一定の制約に従う必要があります。これは短期生産性と長期生産性のトレードオフです。

Reduxが特に有用なケース:

  • アプリケーションの状態が大量にあり、アプリ内の多くの場所で必要となる場合

  • アプリケーションの状態が時間とともに頻繁に更新される場合

  • 状態を更新するロジックが複雑になる可能性がある場合

  • 中規模〜大規模のコードベースを持ち、多くの開発者が関わる可能性がある場合

すべてのアプリがReduxを必要とするわけではありません。自身が開発しているアプリの種類をよく考え、解決すべき問題に最適なツールを選択してください。

さらに詳しく知りたい方へ

Redux関連ライブラリとツール

Reduxの中核は独立した小型JSライブラリです。一般的に以下のパッケージと併用されます:

Redux Toolkit

Redux ToolkitはReduxロジック記述における推奨アプローチです。Reduxアプリ構築に不可欠なパッケージと関数を提供し、ベストプラクティスの組み込み、タスクの簡略化、一般的なミスの防止、アプリ開発の効率化を実現します。

React-Redux

ReduxはあらゆるUIフレームワークと統合可能ですが、最も頻繁にReactと併用されます。React-Reduxは公式パッケージで、Reactコンポーネントが状態の読み取りやストア更新のアクション発行を通じて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>
)
}

この自律的なアプリは以下の要素で構成されます:

  • 状態(State): アプリを駆動する真実の源

  • ビュー(View): 現在の状態に基づくUIの宣言的記述

  • アクション(Actions): ユーザー操作に基づいて発生し、状態更新をトリガーするイベント

これは**「単方向データフロー」**の小規模な例です:

  • 状態(State)は特定の時点におけるアプリの状態を記述する

  • UIはその状態に基づいてレンダリングされる

  • 何かが発生すると(ユーザーのボタンクリックなど)、発生した内容に基づいて状態が更新される

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

単方向データフロー

しかし、同じ状態を共有して使用する必要がある複数のコンポーネントが存在する場合、特にそれらがアプリケーションの異なる部分に配置されている場合、このシンプルさは崩れる可能性があります。これは親コンポーネントへの「状態のリフトアップ」で解決できることもありますが、常に有効とは限りません。

この問題を解決する一つの方法は、共有状態をコンポーネントから抽出し、コンポーネントツリー外の中央集権的な場所に置くことです。これによりコンポーネントツリーは大きな「ビュー」となり、ツリー内のどこに位置していても、どのコンポーネントも状態にアクセスしたりアクションをトリガーしたりできるようになります。

状態管理に関わる概念を定義して分離し、ビューと状態の間の独立性を維持するルールを適用することで、コードの構造化と保守性を高めることができます。

これがReduxの基本概念です。アプリケーションのグローバル状態を一元管理する場所と、状態更新時にコードを予測可能にする特定のパターンを提供します。

不変性

「Mutable(可変)」とは「変更可能」を意味します。「Immutable(不変)」なものは決して変更できません。

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')

ReactとReduxはすべての状態更新が不変的に行われることを前提としています。これが重要な理由と場所については後述します。また、不変的な更新ロジックを記述するより簡単な方法についても説明します。

もっと知りたいですか?

JavaScriptにおける不変性の動作についての詳細は以下を参照してください:

用語

続ける前に理解しておくべき重要なRedux用語があります:

アクション

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

typeフィールドは"todos/todoAdded"のように説明的な名前を持つ文字列であるべきです。通常、タイプ文字列は"domain/eventName"形式で記述します。前半はアクションが属する機能やカテゴリ、後半は発生した具体的な事象を示します。

アクションオブジェクトには発生した内容に関する追加情報を含む他のフィールドを持たせられます。慣例的にこの情報はpayloadフィールドに配置します。

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

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

アクションクリエーター

アクションクリエーター

const addTodo = text => {
return {
type: 'todos/todoAdded',
payload: text
}
}

レデューサー

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

情報

「リデューサー」関数の名称は、Array.reduce()メソッドに渡すコールバック関数と類似していることに由来します。

Reducerは常に以下のルールに従わなければなりません:

  • stateaction 引数に基づいてのみ新しい状態値を計算する

  • 既存の state を変更してはならない。代わりに、既存の state をコピーしコピーした値を変更するという不変的な更新を行う

  • 「純粋」でなければならない - 非同期ロジックの実行、ランダム値の計算、その他の「副作用」を引き起こしてはならない

これらのルールの重要性と正しい実装方法については後ほど詳しく説明します。

Reducer関数内のロジックは通常、次の一連のステップに従います:

  • このアクションが対象かどうかを確認する

    • 対象の場合、stateをコピーし、新しい値で更新して返す
  • 対象外の場合、既存のstateを変更せずに返す

以下は各Reducerが従うべきステップを示した簡単な例です:

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
}

Reducer内部では、新しいstateを決定するためにあらゆる種類のロジック(if/elseswitch、ループなど)を使用できます。

Detailed Explanation: Why Are They Called 'Reducers?'

The Array.reduce() method lets you take an array of values, process each item in the array one at a time, and return a single final result. You can think of it as "reducing the array down to one value".

Array.reduce() takes a callback function as an argument, which will be called one time for each item in the array. It takes two arguments:

  • previousResult, the value that your callback returned last time
  • currentItem, the current item in the array

The first time that the callback runs, there isn't a previousResult available, so we need to also pass in an initial value that will be used as the first previousResult.

If we wanted to add together an array of numbers to find out what the total is, we could write a reduce callback that looks like this:

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

Notice that this addNumbers "reduce callback" function doesn't need to keep track of anything itself. It takes the previousResult and currentItem arguments, does something with them, and returns a new result value.

A Redux reducer function is exactly the same idea as this "reduce callback" function! It takes a "previous result" (the state), and the "current item" (the action object), decides a new state value based on those arguments, and returns that new state.

If we were to create an array of Redux actions, call reduce(), and pass in a reducer function, we'd get a final result the same way:

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}

We can say that Redux reducers reduce a set of actions (over time) into a single state. The difference is that with Array.reduce() it happens all at once, and with Redux, it happens over the lifetime of your running app.

ストア(Store)

現在のReduxアプリケーションの状態は、ストアと呼ばれるオブジェクト内に保持されます。

ストアはReducerを渡して作成され、現在の状態値を返すgetStateメソッドを持ちます:

import { configureStore } from '@reduxjs/toolkit'

const store = configureStore({ reducer: counterReducer })

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

ディスパッチ(Dispatch)

Reduxストアにはdispatchメソッドがあります。状態を更新する唯一の方法は、store.dispatch()を呼び出してアクションオブジェクトを渡すことです。ストアはReducer関数を実行して新しい状態値を保存し、getState()を呼び出すことで更新された値を取得できます:

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

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

アクションのディスパッチは、アプリケーション内で「イベントをトリガーする」と考えることができます。何かが発生したことをストアに通知し、Reducerはイベントリスナーとして機能します。Reducerは関心のあるアクションを検知すると、それに応じて状態を更新します。

通常、適切なアクションをディスパッチするためにアクションクリエーターを呼び出します:

const increment = () => {
return {
type: 'counter/increment'
}
}

store.dispatch(increment())

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

セレクター(Selectors)

セレクターは、ストアの状態値から特定の情報を抽出する方法を知る関数です。アプリケーションが大きくなるにつれ、異なる部分で同じデータを読み取る必要がある場合にロジックの重複を防ぐのに役立ちます:

const selectCounterValue = state => state.value

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

Reduxアプリケーションデータフロー

先に「一方向データフロー」について説明しましたが、これはアプリを更新する次の一連のステップを表しています:

  • 状態(State)は特定の時点におけるアプリの状態を記述する

  • UIはその状態に基づいてレンダリングされる

  • 何かが発生すると(ユーザーのボタンクリックなど)、発生した内容に基づいて状態が更新される

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

Reduxに特化して、これらのステップをさらに詳細に分解できます:

  • 初期設定:

    • ルートReducer関数を使用してReduxストアが作成される
    • ストアはルートReducerを一度呼び出し、戻り値を初期stateとして保存する
    • UIが初めてレンダリングされるとき、UIコンポーネントはReduxストアの現在の状態にアクセスし、そのデータを使用して表示内容を決定する。また、状態が変更されたことを検知できるよう、今後のストア更新をサブスクライブする
  • 更新フロー:

    • ユーザーのボタンクリックなど、アプリ内で何かが発生する
    • アプリコードがReduxストアにアクションをディスパッチ(例: dispatch({type: 'counter/increment'})
    • ストアが前回のstateと現在のactionを使用してリデューサー関数を再実行し、戻り値を新しいstateとして保存
    • ストアが更新されたことを購読している全UIコンポーネントに通知
    • ストアからデータを必要とする各UIコンポーネントが、必要な状態部分に変更があったか確認
    • データが変更された各コンポーネントは新しいデータで再レンダリングを強制し、画面表示を更新

このデータフローを視覚的に表現すると次のようになります:

Reduxデータフロー図

学んだこと

Reduxには覚えるべき新しい用語や概念がいくつかあります。ここで学んだ内容をまとめます:

まとめ
  • Reduxはグローバルなアプリケーション状態を管理するライブラリ
    • Reduxは通常、Reactと統合するためのReact-Reduxライブラリと併用
    • Redux ToolkitはReduxロジックを記述する標準的な方法
  • Reduxの更新パターンでは「何が起きたか」と「状態がどう変化するか」が分離
    • アクションtypeフィールドを持つプレーンオブジェクトで、アプリ内で「何が起きたか」を記述
    • リデューサー は前の状態とアクションに基づいて新しい状態値を計算する関数
    • Redux ストア はアクションが_ディスパッチ_されるたびにルートリデューサーを実行
  • Reduxは「単方向データフロー」アプリ構造を採用
    • 状態は特定時点のアプリ状態を記述し、UIはその状態に基づいてレンダリング
    • アプリで何かが発生すると:
      • UIがアクションをディスパッチ
      • ストアがリデューサーを実行し、発生した内容に基づいて状態を更新
      • ストアが状態変更をUIに通知
    • UIは新しい状態に基づいて再レンダリング

次のステップ

Reduxアプリの個々の構成要素について確認しました。次はパート2: Redux Toolkitアプリ構造に進み、実際の動作例を通じてこれらの要素がどのように連携するかを見ていきましょう。