LoginSignup
5
9

More than 3 years have passed since last update.

FluxによるReactアプリの状態管理 Redux・React-Redux編

Last updated at Posted at 2019-06-09

Redux公式サイトのチュートリアルで学習した内容をまとめたものです。
基本的には、本家サイトに記載されている内容を自分なりに日本語でまとめなおしたものです。
2019年5月時点の情報を基にしています。

Reactの基本事項は既に習得されている方向けです。

概要

Reduxとは

Reduxは、JavaScriptアプリケーションの状態管理を容易にする為のフレームワークです。
主にReactを用いて構築されたアプリケーションで利用されますが、その他のフレームワークやライブラリーで構築された場合も、もちろん利用できます。

アプリケーション上で表示されるデータや、各コンポーネントの表示非表示の状況を一元的に管理することで、見通しのよいアプリケーションの構築を実現させようというものです。
Reactの提供元であるFaceBookが提唱した、Fluxという設計パターンからヒントを得て作られています。

Redux

React-Reduxとは

名前のとおり、ReactコンポーネントとReduxを接続する為のフレームワークです。
React・Reduxに依存した造りというわけではないため、AngularVueなどのその他のフレームワークやVanillaJSでの利用も可能とのことです。

React Redux

Fluxとは

Reactによるアプリケーション構築を保管する為に考案された、設計パターンです。
データの流れを一方通行にすること、アプリケーションの状態「State」を一元的に管理することで、アプリケーションの管理が煩雑化することを防ぐ試みです。
Fluxによるアプリケーションは、下記のパートで構成されます。

  • Dispatcher: アプリケーションへの更新情報を集約・通知
  • Store: アプリケーションの状態を保持・管理
  • Action: アプリケーションの更新情報を得る為の内部API 
  • View: Reactコンポーネントによるインターフェース

Dispatcherは、アプリケーションに1つだけ存在します。
Storeは、アプリケーションの規模などに応じて1つ以上存在します。
Actionは、アプリケーションに発生しうる変更の数だけ存在します。
Vewは、Storeと接続したコンポーネントを頂点としてツリー構造を形成します。

詳細はこちらの記事で扱っております。
FluxによるReactアプリの状態管理 Flux・FluxUtils編

本家サイトはこちら。
Flux

インストール

npmのパッケージとして公開されています。

npm install redux react-redux --save

introduction > installation | Redux
Itroduction > Quick Start | React Redux

Todoアプリ

本家サイトのベーシックチュートリアルには、Todoアプリの解説とソースコードがあります。
ちょくちょく更新されているようです。
2019年5月に写経したソースはこちらです。
todoapp-flux-redux
動作のサンプル

ReactのJSX記法を利用する為、Babelなどのトランスパイラを利用します。
webpackなどのモジュールバンドラーの利用も前提としています。
今回は、Parcelを利用しています。

Actions

Actionは、Storeに対して送信するデータです。
実体はJavaScriptのオブジェクトです。
Actionの種類を示す、typeプロパティを必ず持っています。typeプロパティの値は、一般的に文字列が用いられます。
アプリケーションの規模が大きい場合は、予め定数として定義しておくことが望ましいでしょう。
その他Storeに送信したいデータが含まれます。

新規Todoの追加時のAction
// ActionType
const ADD_TODO = 'ADD_TODO'

// Action
{
  type: ADD_TODO,
  text: 'Build my first Redux app'
}

このActionを生成するのがActionCreatorです。
ReduxにおけるActionCreatorは、単純にActionを返すのみです。

actions/index.js
let nextTodoId = 0;

/**
 * TODOリストを追加する
 */
export const addTodo = text => ({
  type: 'ADD_TODO',
  id: nextTodoId++,
  text
});

/**
 * 表示フィルターをかける
 */
export const setVisibilityFilter = filter => ({
  type: 'SET_VISIBILITY_FILTER',
  filter
});

/**
 * 完了未完了切り替える
 */
export const toggleTodo = id => ({
  type: 'TOGGLE_TODO',
  id
});

/**
 * フィルターの定数
 */
export const VisibilityFilters = {
  SHOW_ALL: 'SHOW_ALL',
  SHOW_COMPLETED: 'SHOW_COMPLETED',
  SHOW_ACTIVE: 'SHOW_ACTIVE'
};

この戻り値をStoreに送信するには、後述するStoreのdispatch()メソッドを利用します。

store.dispatch(addTodo('Learn about Redux'))

Actions | Redux

Reducers

Actionはあくまで「何が起きたか」を定義するだけです。それを受けて「どうするか」に関する具体的な指示は含みません。
その責務を負うのがReducerです。Reducerは、Storeに送信されたActionをもとに、Storeが保持するアプリケーションのStateを、どう変更するかを決めます。

Reducers | Redux

アプリケーションのStateの設計

Reduxでは、アプリケーションの状態「State」は一つのオブジェクトで表されます。
今回のTodoアプリのStateは、以下のようになります。

{
  visibilityFilter: 'SHOW_ALL',
  todos: [
    {
      id: 1,
      text: 'Write a article about Redux',
      completed: true
    },
    {
      id: 2,
      text: 'Go to the Cats cafee',
      completed: false
    }
  ]
}

Stateを変更する

Reducerは、副作用のない純粋な関数です。
Storeから呼び出され、引数で以前のStateと新たに生成されたActionを受け取り、新しいStateを返します。

(previousState, action) => newState

previousStateactionの内容が同一であれば、何度呼び出しても同じnewStateを返します。
その為、Reducerには下記のルールがあります。

  • 引数を変更してはいけません。
  • APIの呼び出しやルーティングなどの、副作用が発生する処理を記述してはいけません。
  • 純粋でない関数を呼び出してはいけません。(例:Date.now(), Math.random())

上記Stateを変更するStateは下記のようになります。

import { VisibilityFilters } from './actions';

const initialState = {
  visibilityFilter: VisibilityFilters.SHOW_ALL,
  todos:[]
};

function todoApp(state = initialState, action){
  switch(action.type){
    case 'ADD_TODO':
      return Object.assign({}, state, {
        todos: [/* 項目追加 */]
      });
    case 'TOGGLE_TODO':
      return // 項目切替後newState
    case 'SET_VISIBILITY_FILTER':
      return {
        visibilityFilter: action.filter
      };
    default:
      return state;
  }
}

初回呼び出し時、引数stateundefinedの状態で渡されます。
その為、デフォルト引数には、Stateの初期値を設定しています。
ActionTypeでActionの種類が「追加」であることを識別し、以前StateにActionの情報を加味した新しいStateを作成しています。
Stateを変更する必要がない場合は、以前のStateをそのまま返します。
Reactは、Stateの変更を確認した上で再描画を実行しています。
その際の比較方法は値ではなく、オブジェクト参照元の比較です。
その為、Reducer内でStateを変更する場合は、引数で受け取った以前のStateをそのまま変更してはいけません。
Object​.assign()スプレッド構文等を利用して、予めStateを複製した上で変更する必要があります。

Reduxユーザーが最もハマるstateの不正変更とその検出方法
Why is immutability required by Redux? | Redux

Reducerの分割

アプリケーションの規模が大きい場合、StateおよびReducerのサイズも大きくなってきます。
その場合、一つのReducerで管理することが難しくなる為、適宜分割することが望ましいでしょう。

todosvisibilityFilter、それぞれ管理するReducerに分割します。
初期値も、それぞれの項目の初期値のみ用意します。

reducers/todos.js
const todos = (state = [], action) => {
  switch (action.type) {
    case 'ADD_TODO':
      return [
        ...state,
        {
          id: action.id,
          text: action.text,
          completed: false
        }
      ];
    case 'TOGGLE_TODO':
      return state.map(todo =>
        todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
      );
    default:
      return state;
  }
};

export default todos;
reducers/visibilityFilter.js
import { VisibilityFilters } from '../actions';

const visibilityFilter = (state = VisibilityFilters.SHOW_ALL, action) => {
  switch (action.type) {
    case 'SET_VISIBILITY_FILTER':
      return action.filter;
    default:
      return state;
  }
};

export default visibilityFilter;

Reducerの結合

分割したものの、Storeから参照されるReducerは一つだけです。
そのため、各ReducerはReduxが提供するcombineReducers()関数を利用して、一つに束ねます。
この関数は各Reducerから返されるStateのパーツを、一つのオブジェクトにまとめたStateにして返す、RootReducerを生成します。

import { combineReducers } from 'redux';
import todos from './todos';
import visibilityFilter from './visibilityFilter';

export default combineReducers({
  todos,
  visibilityFilter
});

// [参考]自作した場合
function roodReducer(state = {}, action){
  return {
    todos: todos(state.todos, action),
    visibilityFilter: visibilityFilter(state.visibilityFilter, action)
  };
}

combineReducers(reducers) | Redux

Store

アプリケーションのStateを一元的に管理するモジュールです。
アプリケーションに1つだけ存在します。
下記の責務を負います。

  • アプリケーションの現在のStateを保持する
  • getState()メソッドを通じて、Stateを提供する
  • dispatch(action)メソッドを通じて、Stateの更新を可能にする
  • subscribe(listener)メソッドを通じて、Stateの更新イベントのリスナーを登録する
  • subscribe(listener)メソッドの戻り値として、イベントリスナー解除用関数を提供する

Fluxでは独立したモジュールとして存在したDispatcherは、Storeのメソッドになりました。
実行順序の調整などは行いません。
Storeは、Actionの受付とState更新の通知を行いますが、新しい状態を生成することはしません。
それはReducerの責務です。
dispatch()が実行されると、受け取ったActionとStoreが保持している現在のStateをReducerに渡し、新しいStateを受け取ります。

Reduxで提供しているcreateStore関数に、RootReducerを渡して作成します。

import { createStore } from 'redux';
import rootReducer from './reducers/';

const store = createStore(rootReducer);

// 第二引数に初期Stateを渡すことも可能
const store = createStore(rootReducer, initialState);

基本的にはこれだけです。こちらで何か処理を書くことはありません。

Store | Redux

ReduxとReactの接続

StoreとReactComponentの接続

StoreとViewを、React-Reduxが提供しているProviderコンポーネントを利用して繋げます。
Reduxで用意したstoreをProviderコンポーネントにpropsとして渡します。

import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import rootReducer from './reducers/';
import App from './components/app';

const store = createStore(rootReducer);

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

これで、View側がStoreの持つStateを参照する為の準備が出来ました。

アプリケーション内でStoreのデータを共有する

Reactでは基本的に、propsを通じて親コンポーネントから子孫コンポーネントへとデータを受け渡します。
コンポーネントの階層が深くなると、データのバケツリレーが発生してしまいます。
React-Reduxが提供しているconnect関数を利用することで、Storeとの接点を持つProviderコンポーネントへのショートカットを作ります。

import { connect } from 'react-redux';
import { someActionA, someActionB } from '../actions';

// 子孫コンポーネントに必要なStateを抽出(任意)
const mapStateToProps = ({paramA, paramB}) => {
  return {
    paramA,
    paramB
  }
};

// 子孫コンポーネントに渡すコールバック関数を定義(任意)
const mapDispatchToProps = dispatch => {
  return {
    onClick: data => dispatch(someActionA(data)),
    onChange: data => dispatch(someActionB(data))
  };
};

// ChildComponentをラップしたコンポーネントを作成
export default connect(
  mapStateToProps,
  mapDispatchToProps
)(ChildComponent);

// Stateの抽出や、コールバック関数の定義が不要な場合
export default connect()(ChildComponent);


// 下位コンポーネントはpropsで受け取れる
const ChildComponent = ({paramA, onClick, ...otherProps})=>{
  return (
    <div>
      <button onClick={e=> onClick(paramA)}>{paramA}<button>
      <GrandchildComponent {...otherProps} />
    </div>
  );
};

connect()関数の部分はカリー化が含まれていてややこしいですが、connect()の戻り値は関数です。それをさらに実行すると、Reactのコンポーネントが作成されます。
これにより、ChildComponentはpropsとしてStateの全データを受け取ることが出来ます。

一部分だけ渡したい場合は、mapStateToProps関数をconnect()の第一引数に渡します。
この関数はStateが更新されるたびに呼ばれ、引数でStateを受け取ります。
そして、戻り値が下位のコンポーネントへpropsとして渡されます。
第二引数のmapDispatchToProps()は、StoreにActionを送信する処理を定義したい場合に利用します。
Store.dispatch()メソッドをを引数で受け取るので、ActionCreatorと共にコールバック関数を定義します。
この関数は初回に実行され、以降は戻り値がpropsの一つとして下位コンポーネントに渡されます。

PresentationalComponents と ContainerComponents

ReactとReduxでアプリケーションを構築する際は、基本的にコンポーネントをPresentationalComponentとContainerComponentの2種に分けます。
PresentationalComponentはビジネスロジックを持たず、描画に徹します。
ContainerComponentはReduxと接点を持ち、PresentationalComponentに必要なpropsを受け渡します。
こうすることで、より見通しの良いアプリケーションの構築を可能とし、コンポーネントの再利用性を高めます。

Presentational Components Container Components
目的 どのように見せるか どのように動くか
Reduxを意識
しているか
しない する
データの提供元 propsから ReduxのStateから
データの変更 propsの
コールバック関数を実行
Reduxのメソッドを実行
作成方法 自分で 通常は、
ReactReduxを利用して

Presentational and Container Components | Redux

TodoListの場合

Todoリストを表示するコンポーネントのツリーは下記のとおりです。
VisibleTodoListがContainerComponentsです。

Provider
  └ VisibleTodoList ☆
      └ TodoList
          └ Todo

Stateからtodoリストの配列を取り出し、さらに現在のフィルターにマッチするものを抽出して、下位のコンポーネントに渡しています。
また、項目をクリック時に完了状況を切り替えるためのコールバック関数も定義しています。

visibleTodoList.jsx
import { connect } from 'react-redux';
import { toggleTodo } from '../actions';
import TodoList from '../components/todoList';
import { VisibilityFilters } from '../actions';

const getVisibleTodos = (todos, filter) => {
  switch (filter) {
    case VisibilityFilters.SHOW_ALL:
      return todos;
    case VisibilityFilters.SHOW_COMPLETED:
      return todos.filter(t => t.completed);
    case VisibilityFilters.SHOW_ACTIVE:
      return todos.filter(t => !t.completed);
    default:
      throw new Error('Unknown filter' + filter);
  }
};

const mapStateToProps = state => {
  return {
    todos: getVisibleTodos(state.todos, state.visibilityFilter)
  }
};

const mapDispatchToProps = dispatch => {
  return {
    toggleTodo: id => dispatch(toggleTodo(id))
  };
};

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

Todo項目のコンポーネントは、受け取った情報を表示したり、コールバック関数を実行するだけです。

todo.jsx
const Todo = ({ onClick, completed, text }) => (
  <li
    onClick={onClick}
    style={{
      textDecoration: completed ? 'line-through' : 'none'
    }}
  >
    {text}
  </li>
);

AddTodoの場合

Todoに項目を追加するコンポーネントを構築します。
Form要素を含むコンポーネントなど、責務の分離が難しい場合は、分離しないという選択肢もあります。
mapStateToProps()を利用しない場合は全Stateを受け取ることになるので、分割代入を利用して必要分だけ受け取っています。

import React from 'react';
import { connect } from 'react-redux';
import { addTodo } from '../actions';

let AddTodo = ({ dispatch }) => {
  let input;

  return (
    <div>
      <form
        onSubmit={e => {
          e.preventDefault();
          if (!input.value.trim()) {
            return;
          }
          dispatch(addTodo(input.value));
          input.value = '';
        }}
      >
        <input ref={node => (input = node)} />
        <button type="submit">Add Todo</button>
      </form>
    </div>
  );
};

export default connect()(AddTodo);

Designing Other Components | Redux

イメージ図

無謀にも、自分なりにA4にまとめてみました。
TodoList_Redux.PNG

5
9
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
5
9