LoginSignup
27
23

More than 3 years have passed since last update.

Reduxによる状態管理の仕組みを理解しよう

Posted at

目次

概要

この記事では、状態管理を行うためのフレームワークであるReduxの基礎や状態管理の仕組みについてまとめています。現在Reduxについて勉強中の方の参考になるようでしたら幸いです。
なお本記事はReactを用いていることを前提条件としています。

(注意)
またReact ComponentとRedux Storeを関連づける手法として、現在はHooksとReduxを用いた手法もありますが、今回は従来のconnect関数を用いた手法で紹介しています。後日Hooksを用いた手法についても投稿予定です。

Reduxとは

Reduxとは、上記でも述べたようにReactの状態(state)を管理するフレームワークです。
またReduxはReactと併用することを想定して生み出されているため、Reactと非常に相性が良いとされています。

ReduxはFluxアーキテクチャの一つで、コンポーネントの数が多くなったときに簡単にstateを共有するための手段として利用されています。

状態(state)の共有イメージ
redux.001.png

またReduxなどのFluxアーキテクチャの最大の特長はデータフローが単方向で構築できることで、規模が大きくなった場合にもデータの流れを見失いにくくなります。(後ほど図解)

Reduxを使用するためには以下のコマンドでインストールしておく必要があります。

$ yarn add redux react-redux

Reduxの要素

Reduxによる状態管理を行うための構成要素には主に以下の4つが必要となります。

  • Action:アプリケーション内でなにが起きたのかを示すオブジェクト(データ)
  • Reducer:Actionのtype(種類)に応じてstateを変化させるメソッド
  • Store:アプリケーション内の全てのstateを保持している場所
  • State:アプリケーションの状態

Reduxのデータフロー

上記のような構成にすることで、先ほど述べたデータフローの単方向化を実現することが可能にすることができます。
redux.002.png

  1. ActionCreatorによってActionを生成
  2. Actionをdispatch
  3. ReducerでStore内のStateを更新
  4. Store内のStateをReact Componentで参照(ReactとReduxの連携)

では次にAction、Reducer、Store、ReactとReduxの連携方法について詳しく説明します。
(stateについては省略します)

Action

Actionの特徴は以下のことが挙げられます。

  • アプリケーションの中でなにが起きたかを示すオブジェクト(データ)である
  • typeとそれに対応する値を持つ(typeの値はユニークなものにする)
  • Storeの唯一の情報源
  • ActionCreatorによって生成される

ActionCreator

Actionを作成するメソッド
FluxにもActionCreatorがありますが、ReduxではActionを作成するのみでStoreへのdispatchは行わないという違いがあります。

例えばToDoリストアプリケーションでリストを追加したいときにはActionCreatorと組み合わせて次のように記述します。


const ADD_TODO = 'ADD_TODO';

// Action Creator
const addTodo = text => {
  return {
    // Action
    type: ADD_TODO,
    text
  }
}

前述の通り、ActionCreatorによって生成されたActionは、生成されたのみでStoreへdispatchされていません。
Reduxではdispatch()メソッドによってActionをStoreに送ることができます。

dispatch(addTodo(text));

Reducer

Reducerの特徴は以下のことが挙げられます。

  • Actionのtypeに応じて状態をどう変化させるのかを定義したメソッドである
  • 引数のstateを更新するのではなく、新しいStateを作成して返す
  • 純粋関数でないとならない(毎回必ず同じ結果を返す)

Reducerの実装手順は以下のようになります。

  1. 状態(state)はオブジェクトとして初期値を定義
  2. Reducerは関数として定義、引数は2つ(state, action)
  3. Actionのtypeに応じて状態を変化させ結果を返す

コードで見ると以下のようなイメージになります。

src/reducers/reducer1
import { ADD_TODO, REMOVE_TODO } from '../actions';

const initialState = { text: 'initial text' };

export default (state = initialState, action) => {
  switch(action.type) {
    case ADD_TODO:
      return {
        // ActionがADD_TODOの場合のState更新処理
      };
    case REMOVE_ADD:
      return {
        // ActionがREMOVE_TODOの場合のState更新処理
      };
    default:
      return state;  // Actionのtypeに当てはまらない場合はデフォルトを返す
  }
};

ToDoリストとして正しいinitialStateの設定かは分かりませんが、Reducerの実装イメージとしてはこんな感じになります。

またReduxではアプリケーション内に存在する全てのReducerを1つのReducerとして結合することできます。

src/reducers/index.js
import { combineReducers } from 'redux';  // このimportが必要
import reducer1 from './reducer1';
import reducer2 from './reducer2';

export default combineReducers({ reducer1, reducer2 });

Store

Storeの特徴は以下のことが挙げられます。

  • Storeはアプリケーション内で唯一のもの
  • アプリケーション内のStateが全て集約されている
  • 従来のReactではpropsを目的のコンポーネントまでバケツリレー形式で渡していたが、<Provider>によりその必要がなくなる(react-reduxからインポート)

Storeは以下のように作成します。

src/index.js
import {createStore} from 'redux';
import reducer from './reducers';

const store = createStore(reducer);

connect関数

ReactとReduxは互いに独立しているため、なにもしないままだとReact ComponentをReduxのフローに乗せることができません。そこでReact Reduxが提供しているconnect()関数を利用して、ReduxのStateやActionとReact Componentを接続します。このとき接続されたComponentはContainerと呼ばれることもあります。

このようにconnect()関数によってContainerではStoreから必要なデータ(Stateの一部)と、ActionをStoreに渡すためのdispatch()関数を使用することができます。
基本的な記述方法としては以下のように書きます。

connect(mapStateToProps, mapDispatchToProps)(App);

ここでconnect()関数の引数に渡されているmapStateToPropsmapDispatchToPropsについて詳しく見ていきましょう。

mapStateToProps

StoreにあるStateの情報からContainerで必要な情報を取り出し、Container内のpropsとしてマッピングする機能を持つ関数です。mapStateとして呼ばれることもあります。
上記でも示したようにconnect()関数の第1引数になります。

呼ばれるタイミング Storeの状態が変化したとき
第1引数 State(Storeの全ての状態)
第2引数(オプション) 自身のprops(ownProps)
戻り値 Containerが必要としているStateをpropsとして返す
const mapStateToProps = state => {
  return {
    // 全体のStateから必要な情報 (state.valueなど)
    // value: state.value
    // のように記述
  };
};

ownPropsを第2引数に設定した場合、最終的な戻り値にはownPropsと新たに得たデータが1つのpropsとしてマージされます。

mapDispatchToProps

ActionをStoreにdispatchするdispatch()関数をpropsにマッピングするために使用されます。mapDispatchとして呼ばれることもあります。
dispatch()関数とは、あるActionが発生したときにReducerにtypeに応じた状態遷移を実行させるための関数です。Storeに標準でdispatch()関数が用意されています。

mapDispatchToPropsconnect()関数の引数に指定しなくてもコンポーネントはデフォルトでprops.dispatchを受け取れるので、これを使用してStoreにActionをdispatchすることができます。

ではmapDispatchToPropsを使用する理由はなんでしょうか?
例として、「ボタンを押すとvalueが+1される」というContainerを見てみましょう。

mapDispatchToPropsを使用しない場合
Container側でActionCreatorによってActionを作成した段階ではdispatchされていないので以下のように記述する必要があります。

import React, { Component } from 'react';
import { connect } from 'react-redux';
import { increment } from '../actions';

class Counter extends Component {
  render(){
    const props = this.props;
    return (
      <React.Fragment>
        <div>count: {props.value}</div>
        <button onClick={() => dispatch(increment())}>+1</button>  {// ここ }
      </React.Fragment>
    );
  }
}

const mapStateToProps = state => ({ value: state.count.value});
export default connect(mapStateToProps /*, 第2引数なし(mapDispatchToProps) */)(Counter);

若干JSXの記述が複雑になっています。

mapDispatchToPropsを使用した場合

import React, { Component } from 'react';
import { connect } from 'react-redux';
import { increment } from '../actions';

class Counter extends Component {
  render(){
    const props = this.props;
    return (
      <React.Fragment>
        <div>value: {props.value}</div>
        <button onClick={props.increment}>+1</button>
      </React.Fragment>
    );
  }
}

const mapStateToProps = state => ({ value: state.count.value});
const mapDispatchToProps = ({ increment });
// 上記は下の記述と同じ意味
// const mapDispatchToProps = dispatch => ({
//   increment: () => dispatch(increment())
// });

export default connect(mapStateToProps, mapDispatchToProps)(App);

このように記述することができます。
mapDispatchToProps内ではActionCreatorでActionを生成しdispatchまで行う処理を関数に定義してpropsに渡しています

今回は処理が少ない場合でしたのであまり影響はありませんが、Actionの種類や作成の数が多くなった場合にはmapDispatchToPropsを使用することでContainer側で毎回dispatch処理を記述する必要がなくなります。

呼ばれるタイミング ActionCreatorが呼び出されたとき
(今回はクリックされたとき)
第1引数 dispatch()関数
第2引数(オプション) 自身のprops(ownProps)
戻り値 指定したActionのdispatch処理をpropsとして返す

第2引数としてownPropsを設定した場合、Containerが新しいpropsを受け取ったタイミングでmapDispatchPropsが呼び出されます。

Provider

Component階層の最上位のComponentを<Provider>でネストすることで全体のComponentをStoreに接続することが可能になります。
それによって任意のComponentでconnect()関数を用いてStoreと接続できます。

コードの記述イメージは以下のようになります。

src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';

import { App } from './App';
import createStore from './createReduxStore';

const store = createStore();

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

まとめ

長くなりましたが、Reduxの基礎については以上にしたいと思います。
では簡単に今回の記事の内容をまとめます。

  • Redux:アプリケーションの状態管理を行うためのフレームワーク
  • Action:アプリケーションでなにが起きたのかを示すオブジェクトデータ
  • Reducer:Actionの種類に応じて状態を変化させるメソッド
  • Store:アプリケーションの全ての状態を保持している場所
  • connect():React ComponentとReduxを接続するためのメソッド
  • Provider:connect()でComponentをStoreに接続するために必要なもの

各要素の役割とデータフローをしっかり抑えておくことが重要です。
Reduxについては個人的に理解するのがなかなか大変で、まだ完全には理解できていない面もあるので今後も勉強していきたいと思います。

参考資料

27
23
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
27
23