当ブログの記事でReact学習されている読者様やそうでない初心者様もReactを学び始めて、Reduxという用語を何度も聞いたことがあるかもしれません。
特にWeb開発の世界に不慣れな方は、名前を聞いたことがあるかもしれませんが、ほとんどの方々は『 Reduxとは何ですか? 』と疑問に思っている方も多くいらっしゃるかと思います。
それを解決し、仕組みと使い方を学んでいきましょう。
この記事では、Reduxとは何かを説明し、その主な機能について説明します。
Reduxとは?
Reduxは、Reactアプリケーションの状態管理ライブラリであり、アプリケーション内の状態をより効率的に管理するために使用されます。
毎月に何百・何万回もダウンロードされ、すべてのReactプロジェクトの50%以上がそれを使用しています。
ほとんどのReact開発者によって、Webアプリとモバイルアプリの両方での小規模および大規模なReactプロジェクトに採用されております。
要するに、ReduxはReactでの状態を管理する方法であり、構造化された方法ですべてのコンポーネントからアクセスできるキャッシュまたはストレージであるとも言えます。
直接DOMレンダリングを削除し、状態を1か所で管理するのに役立つものです。
なぜ、Reduxなのか?
状態は、多くの場合、データを表示するコンポーネントで直接管理されます。
たとえば、コンポーネントはフォーム入力の現在の値を独自の状態に保存し、ユーザーの入力に応じてその状態を更新する場合があります。
アプリケーションのサイズと複雑さが増すにつれて、この状態の管理が難しくなる可能性があります。
つまりReactでは、データがどのコンポーネントから来ているかを追跡するのが難しいため、コンポーネント間の状態転送はかなり面倒です。
ユーザーがアプリケーション内で多数の状態を操作している場合、非常に複雑になります。
ここでReduxの出番です。
Reduxは、ストアと呼ばれる1つの場所にすべての状態を格納することで、状態転送の問題を解決します。
そのため、すべての状態が同じ倉庫(ストア)に格納されるため、状態の管理と転送が容易になります。
アプリケーション内のすべてのコンポーネントは、そのストアから必要な状態に直接アクセスできます。
これだけでは、いまいち分からないと思いますのでここから詳しく解説していきます。
Reduxの仕組み
Reduxの仕組みは難しい事ではなく簡単です。
各コンポーネントは、あるコンポーネントから別のコンポーネントにPropsを送ることなく、保存された状態にアクセスできます。
つまり、下記の3つの最も重要なReduxの原則があります。
Reduxの基本的な構成要素は下記となります。
・ Store(ストア)
・ action(アクション)
・ reducer(レデューサー)
Reduxは、アプリケーション内の状態を単一のストアに格納し、アプリケーション内のすべてのコンポーネントがこのストアにアクセスできるようにします。
コンポーネントは、ストア内の状態を更新するためのアクションをディスパッチすることができます。
ストアは、アクションを受け取り、それに応じて状態を更新するためのレデューサーを呼び出します。
このアーキテクチャは、アプリケーション内の状態変更を追跡し、管理することを簡単にします。
重要なのが、アプリケーションの状態全体を保持する中央ストアです。
Reduxツールに習熟するには、これらのコンポーネントまたは基本的な構成要素についての十分な知識が必要となります。
それぞれが何をするのか簡単に説明していきます。
Reduxのアクション
Reduxのアクション(action)は、アプリケーション内で発生するイベントや状態変更のためのオブジェクトであり、そのイベントや変更を説明するためにタイプ(type)プロパティを持ちます。
また、任意の追加のデータを含めることができます。
Reduxのアクションは、アプリケーション内のあらゆる場所からディスパッチ(dispatch)されます。
アクションをディスパッチすることで、Reduxストアに対して変更をリクエストし、ストア内の状態を変更することができます。
アプリケーションからReduxストアにデータを送信できる唯一の方法となっています。
たとえば、カウンターアプリの場合、インクリメントボタンがクリックされた場合、ストア内のカウンターの値を増やすためのアクションを作成することができます。
そのアクションは、下記のようになります。
{ type: 'INCREMENT' }
このアクションは、Reduxストアに送信され、ストアは、アプリケーション内で定義されたReducerによって処理されます。
Reducerは、現在の状態とアクションを使用して、新しい状態を生成するための純粋な関数です。
アクションがReducerによって処理されると、ストア内の状態が更新され、アプリケーションのビューに反映されます。
Reduxアクションの役割は以下の通りです。
・ アプリケーション内で起こったイベントを記録する
Reduxのアクションは、アプリケーション内で何が起こったのかを記録するために使用されます。例えば、ユーザーが「ログイン」ボタンをクリックした場合、ログインアクションが発生します。
・ ストアにデータを送信する
アクションは、ストアにデータを送信するために使用されます。例えば、新しいタスクが追加された場合、アプリケーションはタスク追加アクションをディスパッチし、ストアにタスクを追加するよう指示します。
・ アプリケーションの状態を変更する
Reduxアクションは、アプリケーションの状態を変更するために使用されます。アクションは、ストア内の状態を更新し、新しい状態を生成するように指示します。
・ アプリケーション内のすべてのコンポーネントに通知する
アクションは、アプリケーション内のすべてのコンポーネントに通知するために使用されます。 アクションがディスパッチされると、ストアはサブスクライブされたすべてのコンポーネントに通知し、変更を反映するように指示します。
アクションで処理されるべき情報を含むpayload(ペイロード)を持たなければなりません。
アクションは、アクションクリエイターによって作成されます。
例えば、前述したようにアプリのログインなどでの場合では下記のようにします。
const loginStatus = (name, password) => { { type: "LOGIN", payload: { username: "Taro", password: "xxxx" } } }
アクションはtype
プロパティを含む必要があり、格納される他のペイロードを含む必要があります。
つまり、アクションはアプリケーションからストアにデータを送信する情報のペイロードとなります。
抑えておくべき事は、すべてのデータは最終的にアクションとしてディスパッチされる必要がある事です。
Reduxのアクションは、アプリケーション内の状態管理において非常に重要な役割を果たします。
アプリケーション内で何が起こったのかを明確に示し、状態変更を管理するために必要なすべての情報を提供します。
Reduxのディスパッチ
Reduxのディスパッチ(dispatch)は、アプリケーションの状態(state)を変更するために使用される関数です。
ディスパッチを使用することで、アプリケーション内のどこからでも、アクション(action)を作成し、ストア(store)に送信することができます。
import { createStore } from 'redux' // アクションtypeの定義 const INCREMENT = 'INCREMENT' const DECREMENT = 'DECREMENT' // アクションクリエーターの定義 function increment() { return { type: INCREMENT } } function decrement() { return { type: DECREMENT } } // レデューサーの定義 function counterReducer(state = 0, action) { switch (action.type) { case INCREMENT: return state + 1 case DECREMENT: return state - 1 default: return state } } // ストアの作成 const store = createStore(counterReducer) // ディスパッチを使ってアクションを送信する store.dispatch(increment()) // 状態を1増やす store.dispatch(decrement()) // 状態を1減らす
上記の例では、store.dispatch()
を使用して、increment()
アクションおよびdecrement()
アクションをストアに送信しております。
ストアは、アクションに基づいて状態を変更し、新しい状態を生成します。
それらのアクションがストアに送信された結果、状態が1増えた後に1減ります。
つまり、アプリケーションの状態を更新したいときはいつでも、それらをStoreにディスパッチ(送信)するだけです。
残りはレデューサーによって処理されます。
Reduxのreducer
Reducer(レデューサー)は、アプリケーションの現在の状態(state)を取得し、アクション(action)を実行してから新しい状態を返す純粋関数となります。
言い換えますと、現在の状態とアクションを受け入れ、実行されたアクションとともに新しい状態を返す関数です。
そして状態はオブジェクトとして保存されます。
オブジェクトとして保存し、ストアに送信されたアクションに応答してアプリケーションの状態がどのように変化するかを指定します。
ReducerはJavaScriptのreduce
関数から借用しています、つまりreduce
関数がベースとなっています、コールバック関数が実行された後に複数の値から一つの値を計算します。
Array.prototype.reduce(reducer, ?initialValue)
に渡すような関数なので、これはレデューサー(reducer)と呼ばれます。
単純なカウンターアプリのReducerの例を下記に示します。
// Reducerの定義 const counterReducer = (state = 0, action) => { switch(action.type) { case 'INCREMENT': return state + 1; case 'DECREMENT': return state - 1; default: return state; } }
上記のReducerは、現在のカウンターの状態を表す整数値を受け取ります。
アクションがINCREMENTまたはDECREMENTである場合、Reducerは現在の状態に1
または-1
を加算し、新しい状態を返します。
それ以外の場合、Reducerは現在の状態をそのまま返します。
Reducerを使用することで、状態の変更に関するロジックを明確にし、アプリケーション内の状態の変化を簡単に追跡できるようになります。
Reducerを扱う上での注意点があります。
これは、Reducerの内部で絶対にやってはいけないことです。
・ Reducerの引数を変更する
・ データベース呼び出し、API呼び出し、ルーティング遷移などのサイドエフェクトを実行する。
・ 純粋でない関数を呼び出さないこと、例えばDate.now()
やMath.random()
など
つまり、常に新しい状態を返す純粋な関数である必要があります。
Reducerが外部のデータに依存したり、副作用を持ったりする場合、Reduxストアの予測不可能な動作を引き起こす可能性があります。
使用する上での必ず意識する注意点は下記となります。
・ スプレッド演算子を使用すること
Reducerが新しい状態を返す場合、古い状態を変更するのではなく、スプレッド演算子(…)を使用して新しい状態を作成する必要があります。
その理由は、古い状態を直接変更すると、Reduxストアの不正確な動作を引き起こす可能性がある為です。
・ 複数のReducerを使用すること
アプリケーションが複雑になるにつれて、Reducerを複数作成し、アプリケーション内の複数の状態を管理する必要が常にあります。
Reducerは、同じアクションに対して異なる状態を返すことができ、複数のReducerを組み合わせて複雑なアプリケーション状態を構築することができます。
・ 大規模なアプリ開発では、小さな関数に分割すること
大規模なアプリケーションの場合、Reducerは膨大な量のコードになり、保守が困難になる可能性があります。
その場合、Reducerを小さな関数に分割し、モジュール化することが望ましいです。
これらの注意点を守ることで、ReduxのReducerを効果的に使用することができますので覚えておいて下さい。
ReduxのStore
ストアは、アプリケーションの状態を保持します。
どのReduxアプリケーションでもストアは1つだけにしておくことが強く推奨されています。
つまり、 Reduxアプリには1つのストアのみが存在する必要があるという事です。
状態の保持は、オブジェクトとなっています。
先述で説明してきた、それらをまとめるオブジェクトになります。
ストアには、状態を更新するためのメソッドが含まれております。
保存された状態にアクセスしたり、状態を更新したり、メソッドを使ってリスナーを登録したり解除したりすることも可能です。
ストアの主な役目は下記の通りです。
・ 状態の保持
アプリケーションの状態を保持し、Reduxアプリケーションの他の部分からアクセスできるようにします。
・ アクションの受け取り
アプリケーション内で発生したアクションを受け取り、それを用いて現在の状態を更新します。
・ 状態の更新
アクションを受け取り、それに基づいてアプリケーションの状態を更新します。
・ ストアのサブスクリプション
ストアに変更が加えられた際に、リスナーを通知することができます。
Reduxのストアは、単一の場所にアプリケーションの状態を保持するため、アプリケーション全体で状態の変更を一貫して管理することができます。
これにより、アプリケーションの複雑さを抑え、メンテナンスやテストが簡単になります。
ストアの作成方法を学びましょう。
本日はcreate-react-appを使用します。
すでにインストールされている場合は、下記手順はスキップして下さい。
npm install create-react-app
Reactプロジェクトのアプリを作成します。
npx create-react-app 任意プロジェクト名
必要なnpmパッケージは2つだけとなります。
Redux ToolkitおよびReactReduxパッケージを作成したReactプロジェクトに追加します。
npm install @reduxjs/toolkit react-redux
Reduxのストアを作成するためには、createStore()
関数を使用します。
import { createStore } from 'redux'; createStore()
この関数は、Reducer関数と初期状態を受け取り、Reduxストアを作成します。
下記は、Reduxストアを作成するための基本的な例となります。
import { createStore } from 'redux'; import rootReducer from './reducers'; const initialState = { // 初期状態の設定 }; const store = createStore(rootReducer, initialState);
上記の例では、createStore()
関数を使用して、rootReducer
とinitialState
を指定してReduxストアを作成しています。
rootReducer
は、アプリケーションのすべてのReducer関数をまとめたものです。
initialState
はアプリケーションの初期状態を表します。
Reducer関数を定義して、複数のReducer関数をまとめたrootReducer
を作成しましょう。
import { combineReducers } from 'redux'; import counterReducer from './counterReducer'; import todosReducer from './todosReducer'; const rootReducer = combineReducers({ counter: counterReducer, todos: todosReducer }); export default rootReducer;
上記の例では、combineReducers()
関数を使用して、counterReducer
とtodosReducer
をまとめてrootReducer
を作成しています。
combineReducers()
関数は、引数に渡されたReducer関数を結合して、単一のReducer関数を返します。
rootReducer
は、Reduxストアの作成時にcreateStore()
関数に渡されます。
このように、createStore()
関数を使用することで、Reduxストアを作成することができます。
Reduxストアは、アプリケーション全体で使用されるグローバルな状態を管理するために使用されます。
では、Reduxストア内にある現在のアプリケーションの状態を取得する例を示します。
それを行うには、ReduxのgetState()
メソッドを使用します。
Reduxアプリケーションでは、アプリケーションの状態は単一のストアに集約されています。
getState()
メソッドを使用することで、Reduxストア内の現在の状態を取得することができます。
import store from './store'; const currentState = store.getState();
上記の例では、Reduxストア内の現在の状態をcurrentState
変数に代入しています。
この状態オブジェクトは、Reduxストア内の現在のアプリケーションの状態を表しています。
このように、getState()
メソッドを使用することで、Reduxストア内の現在の状態を簡単に取得することができます。
注意点は、getState()
メソッドは、Reduxストア内の状態を直接変更することはできません。
状態を変更するためには、dispatch()
メソッドを使用する必要があります。
状態をdispatch(action)を介して更新可能にする方法をおさらいしてみましょう。
Reduxのdispatch()
メソッドは、アプリケーション内でアクションを発生させ、Reduxストアに対してそのアクションを送信するためのメソッドです。
Reduxでは、アプリケーション内で何らかのイベントが発生した場合、それを表すアクションオブジェクトを作成し、dispatch()
メソッドを呼び出してReduxストアに送信します。
Reduxストアは、そのアクションに基づいて現在の状態を更新し、更新された状態を全てのコンポーネントに通知します。
これは、アプリケーションの状態を更新する唯一の方法となります。
import store from './store'; store.dispatch({ type: 'ADD_TODO', payload: { text: 'Learn Redux', completed: false } });
上記の例では、dispatch()
メソッドを使用して、アプリケーション内での「新しいタスクを追加する」というアクションをReduxストアに送信しています。
アクションは、type
とpayload
という2つのプロパティを持つオブジェクトとして定義されています。
dispatch()
メソッドは、アクションオブジェクトをReduxストアに送信するだけでなく、そのアクションを処理するためのReducer関数も呼び出します。
このReducer関数は、アクションに基づいてReduxストアの状態を更新します。
subscribe(listener)を介してリスナーを登録する例を示します。
Reduxのsubscribe()
メソッドを使用すると、Reduxストア内の状態の変更を監視するためのリスナーを登録することができます。
import store from './store'; const listener = () => { console.log('Redux state updated:', store.getState()); }; store.subscribe(listener);
上記の例では、subscribe()
メソッドを使用してlistener
関数をReduxストアに登録しています。
listener
関数は、Reduxストアの状態が更新されたときに呼び出されます。
store.subscribe()
メソッドは、登録されたリスナー関数を返します。
必要に応じて、後でリスナーを解除することもできます。
上記の例では、リスナー関数がReduxストアの状態をコンソールにログ出力するだけですが、実際のアプリケーションでは、リスナー関数を使用してReduxストアの状態を更新するような処理を実行することができます。
それでは、リスナーの登録解除の処理を行う方法です。
Reduxのsubscribe()
メソッドを使用して登録されたリスナーを解除するには、subscribe()
メソッドが返す関数を呼び出します。
この関数を呼び出すことで、登録したリスナーがReduxストアから削除されます。
const listener = () => { console.log('Redux state updated:', store.getState()); }; const unsubscribe = store.subscribe(listener) unsubscribe() //リスナーの解除
unsubscribe()
は、状態が変化したときにリスナーメソッドを呼び出したくない場合にも役立ちます。
Reduxストアの状態が変更されたときに、その変更を監視するためのリスナーを簡単に登録および解除することができます。
ただし、過剰な使用はパフォーマンスに影響を与える場合があるので、適切なタイミングでリスナーを登録する必要がありますのでご注意下さい。
Reduxはまだ正しい選択なのか?
まず、Reduxは、アプリケーションの状態管理のために設計されたライブラリであり、データを保存するためには適していません。
Reduxは、アプリケーション内のコンポーネント間で共有される状態を管理するために使用されます。
この状態は、ユーザーがアプリケーションで実行する操作によって変更され、Reduxによって管理されます。
一方で、データの保存には、通常は永続的なデータストレージが必要です。
Reduxは、一時的なアプリケーションの状態管理に適していますが、永続的なデータ保存には適していません。
永続的なデータ保存が必要な場合は、代わりにデータベースやファイルシステムなどの方法を使用する必要があります。
ですが、データ管理は非常に困難です。
アプリケーションが進化するにつれて、データを管理するための戦略を立てることが不可欠となります。
使用する状態管理ライブラリが何であれ、適切かつ徹底的に設計されたアプリケーションは、開発者の生産性に大きな違いをもたらす可能性があります。
Reactと新しいライブラリのおかげで、Reduxはもはや唯一の選択肢ではなくなってきています。
データとアプリケーションの状態を管理する方法を簡素化するために、他の新しいライブラリとテクノロジーが利用可能です。
Reduxは、データをコンポーネントに渡す問題を解決するのでプロジェクトに組み込まれることがよくあります。
あらゆる種類のデータにどこからでもアクセスできるようになります。
ですが、開発者がよくしがちな間違いの1つにReduxを使いすぎることです。
管理が非常に困難な大規模なデータストアに直面した時に、同時にディスパッチされるアクションが多すぎてアプリケーションのパフォーマンスが低下し、ストアが更新され再レンダリングが増えることがあります。
つまり、アプリケーション内のすべての状態が単一のストアに格納されるため、状態が更新されるたびにアプリケーション全体が再レンダリングされる可能性があります。
これは、アプリケーションのパフォーマンスを低下させる可能性があります。
また、複雑なアプリケーションの状態管理を担当するために多数の概念、アーキテクチャ、APIを提供します。
アプリケーションが非常に複雑である場合、Reduxによって管理されるコードをデバッグすることがより困難になり、コードが複雑化していく可能性があります。
しかしながら、その重要性は否定できずGithubからの月間ダウンロード数は3200万を超えております。
アプリケーションの状態とロジックを一元化することにより、(元に戻す/やり直し)、状態の永続化などの強力な状態管理機能を提供します。
最後に
Reduxは、グローバルな状態管理を必要とする小規模および大規模なアプリケーションを構築するための優れた多数のツールやライブラリを提供します。
これによって、開発者は状態管理に関する重要な問題に対処するために時間を節約でき、生産性が向上します。
単純なケースでは、MobX、PushState、React Contextなどの軽量ツールがございます。
Contextは下記で学習する事ができます。
エンジニアの観点から見ると、Reduxの大きなメリットは、開発者のエクスペリエンスを向上させることです。
複雑なロジックをアプリ全体に分散させるのではなく整理することで、複雑なロジックの処理が容易になります。
その上、アプリケーション内の状態を変更するために厳密なルールと制約を設けることで、状態の変更を予測可能にします。
それにより、開発者は意図せずに状態を変更することを防ぎ、デバッグやテストが容易になります。
そして、状態管理とビジネスロジックのコードを分離するためのアーキテクチャも提供します。
アプリケーションのコードがよりモジュール化され、可読性が向上致します。
上記のような、メリットがあるため、Reduxは多くのReact開発者にとって人気のあるライブラリとなっています。
本日は以上となります。
最後まで読んで頂きありがとうございます。
この記事が気に入ったら、ブックマークし他の方にも共有してください。