Reduxについてざっくりとまとめた

Reduxについてざっくりとまとめた

Reduxとは

Reduxとは状態管理ライブラリです。

Reduxはアプリケーションの状態をストア(Store)と呼ばれる一つのJavaScriptオブジェクトで管理をしている。

ストアに変化を加える方法は、事前に定義されているActionsと呼ばれるJavaScriptのオブジェクトをReducersと呼ばれる関数に渡す(dispatch)する必要があります。

Reduxの処理の流れ

  • コンポーネントが、Action Creator を呼び出して Action を取得する。
  • 取得した Action を Reducer に渡す。これを dispatch という。
  • Reducer は、渡された Action に応じて State を更新する。
  • コンポーネントは State に変更があれば、関連する UI を書き換える。

このルールによりコンポーネントが自由に状態(State)を書き換えられる事がありません。

状態を更新するためには必ずReducerに対して更新処理を依頼する必要があり、さらにReducerは予め決められた属性・アクションを渡さなければなりません。
typeの値など間違ったActionを渡さない様、ActionCreatorを介してActionを発行させています。

Reduxは状態(State)に対して定義された変更しか加えられないので、このルールは予期しない更新処理を防ぎ、コードを予測しやすくします。

Action

Actionは個々のイベントに対する情報のことで、データを追加、更新、削除したりです。

Reduxにおいて状態変化は全てActionを通じて行います。

Actionは固有の名前を持ち、状態変化について必要な情報を持つ必要があります。

Actionはオブジェクト{}
Action CreatorはActionを返す関数 f(x)

Reducer

Actionを使って状態変化を生じさせるにはReducerと呼ばれる関数にActionを送信する必要があります。

ReducerにActionを送信する動作をDispatchと呼びます。

Reducerは元の状態を変更することなく、新しい関数を返す。(そのためReducerで返す関数は純粋関数でなければならない。)

Reducerは状態(State)を返す関数 f(x)

Store

Storeはアプリケーションの状態を保持するオブジェクトである。

ReactなどのフレームワークからStoreを参照することでUIにStoreを反映出来る様になる。

この様にアプリケーションの状態を一箇所で管理し、状態を変化させる方法を制限することでアプリの状態変化を厳密に管理することが可能になる。

Storeはオブジェクト{}

Redux三原則

  • Single source of truth
    アプリの状態を1つのストアで管理するということです。
  • State is read-only
    アプリの状態を変化させるには、必ずActionと呼ばれる事前に定義したJavaScriptのオブジェクトをReducerにdispatchする必要がある。
  • Changes are made with pure functions
    状態変化は純粋関数によって行われなければならない。

Reduxを使ってみる

Reducer

import { combineReducers } from 'redux';

const initstate = {list: []};

export function todos(state = initstate, action) {
  switch (action.type) {
    case "ADD_TO_DO":
      return Object.assign({}, state, {
        list: state.list.concat(action.todo)
      })
    case "REMOVE_TO_DO":
      return Object.assign({}, state, {
        list: state.list.filter(todo => {
          if (action.todo != todo) {
            return true;
          } else {
            return false;
          }
        })
      })
      default:
        return state;
  }
}

export default combineReducers({
  todos
});

Storeの中の個々の要素の構造を制限するのもReducerの役割であり、今回の例ではtodosというReducerを宣言していて、この最初の引数にはstate={list: []}という形式でStoreの要素としてのtodosの構造が定義されている。

Storeの中にはtodosというオブジェクトが確保され、その中にはToDoリストの個々の要素を入れるための配列としてlistというプロパティが確保される。

storeに対して変化を加えるためのActionを宣言するのもReducerの役割であり、todosの第二引数で与えられるactionにはDispatchされたActionが想定される。

このActionは事前に定義されたtypeである必要がある。Reducerに定義されていないタイプのActionがDispatchされた場合はなにも起こらない。

Action

ReducerではStoreの構造を宣言して、それに変化を加えるためのActionタイプを宣言する事を行いました。
(Storeの構造とはtodosというオブジェクトの中に個々の要素を入れるための配列Listを確保している状態)

ここではActionをDispatchしてStoreに変化を加えるための関数をまとめて宣言します。

export function addTodo(todo) {
  return {
    type: 'ADD_TO_DO',
    todo
  }
}

export function removeTodo(todo) {
  return {
    type: 'REMOVE_TO_DO',
    todo
  }
}

/** Reducerで受け取るActionはこの関数に定義されている。 */
return {
  type: 'ADD_TO_DO',
  todo: 'something to do'
}

/** Reducerの第二引数のactionには以下のオブジェクトが与えられる */
{
  type: 'ADD_TO_DO',
  todo: 'something to do'
}

Reducer側ではswitch文で受け取ったactionのtypeに応じて処理を振り分けるので、
今回の例で言えば case: 'ADD_TO_DO'が実行されることになる。

Store

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

const store = createStore(reducers); // reducerを引数にとる

export default store;

reduxライブラリからcreateStoreと先ほど作成したreducersをインポートして、storeを取得する。

React + Redux

reduxは状態管理のライブラリであり、Reactに元から備わっている機能ではないので先ほど説明したStoreやReducer、Actionと紐付ける事が必要になります。

ReactからStoreのデータにアクセスする

React側からStoreの中のデータを利用するためにはreact-reduxライブラリのProviderを使用する必要があります。

ProviderというコンポーネントにStoreを渡す事で子要素のコンポーネントからStoreの情報にアクセス可能になります。

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux'; // 追加
import store from './store';            // 追加  
import App from './App';
import * as serviceWorker from './serviceWorker';

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


serviceWorker.unregister();

次にコンポーネントからStoreにアクセスが出来る様にconnectを使用します。
これはreact-reduxのconnectを読み込みます。

/** 子要素であるAppコンポーネントに以下の記述を加える */
import { connect } from 'react-redux';
import store from './store';
...
export default connect()(App);

Appを外部から参照できる様exportしていましたが代わりにconnectを出力しています。

connect関数を用いてコンポーネントと紐付けは出来ましたが、これだけでは個別のデータにアクセスする事は出来ません。

mapStateToProps

先ほど使用したconnectと合わせてmapStateToPropsという関数(ただ一つのオブジェクトを返す関数)を渡す事でStoreの情報をpropsに反映させる事が可能になります。

mapStateToPropsの引数には自動的に現在のStoreの中身が与えられ、そのStateの中身をどの様な名前で個別のコンポーネントのpropsに渡すかを宣言します。

/** Appコンポーネントに以下の記述を加える */
const mapStateToProps = state => {
  return {
    todos: state.todos.list
  }
}

今回の例ではReducerにtodosというオブジェクトがStoreに格納される様設定しました。
このReducerによってStoreにはtodosというデータが与えられています。
加えて、このtodosにはlistという配列型式の要素がありその中に具体的な要素を対応させています。
これを取り出してコンポーネントのUIに反映させたいです。
{
  todos: 
        list: [
          'something to do',
          'I have to do ~'
        ]
}

mapStateToPropsにはStateという引数が与えられ、そのstateにはStoreの中身が格納されています。
なので、state.todosとする事でReducerで宣言されたtodosを取り出す事が出来ます。

ReactからStoreに変化を加える

この記事最初に説明した様にReduxは事前に定義したActionによって決まった形でしかデータを変更する事が出来ません
データフローは必ず単方向に制限されます。

Storeのデータを改変するにはActionをDispatchする必要があります。

Actionは下記のように既に記述しています。

export function addTodo(todo) {
  return {
    type: 'ADD_TO_DO',
    todo
  }
}
export function removeTodo(todo) {
  return {
    type: 'REMOVE_TO_DO',
    todo
  }
}

この関数をReactのコンポーネントから呼び出し任意の引数を与えdispatchしてReducerに渡す事でStoreに変化を与える事が出来ます。

Providerの子要素でconnectを使用したコンポーネントからはthis.props.dispatchを使ってdispatchを行う事が可能みたいので、これを使ってActionをdispatchします。

この関数(this.props.dispatch)にActionを渡すとそのActionがReducerに渡され、Storeに変化を与えます。

import React, { Component } from 'react';
import { connect } from 'react-redux';
import { addTodo, removeTodo } from './actions' 
import './App.css';

class App extends Component {
  /** 追加 */
  constructor() {
    super();
    this.state = {
      input: '',
    }
  }
  /** 追加 */

  render () {
    return (
      <div className="App">
        <ul>
          {this.props.todos.map(todo => {
            return (
              <li key={todo}>{todo}</li>
            );
          })}
        </ul>

        {/* 追加 */}
        <input 
          type="text"
          onChange={e => this.setState({input: e.target.value})}
        />
        <button onClick={() => this.props.dispatch(addTodo(this.state.input))}>
          追加
        </button>
        {/* 追加 */}
        
      </div>
    );
  }
}

const mapStateToProps = state => {
  console.log(state);
  return {
    todos: state.todos.list
  }
}

export default connect(mapStateToProps)(App);

inputタグに入力された文字をonChangeで取得して、 ボタンがクリックされた時に取得した入力欄の内容をaddTodo()に渡してthis.props.dispatchに渡す事でActionをdispatchしています。

これで入力欄の内容を動的に追加する処理を実装しています。

// 削除処理
<li key={todo}>
  <span>{todo}</span>
  <button onClick={() => {this.props.dispatch(removeTodo(todo))}}>
    削除
  </button>
</li>

mapDispatchToProps

mapStateToProps同様、ただ1つのオブジェクトを返す事の出来る関数を使用可能です。

引数としてdispatchが与えられおり、これを使用する事でActionのdispatchが可能になります。

const mapDispatchToProps = dispatch => {
  return {
    onAddTodo(todo) {
      dispatch(addTodo(todo))
    }
  }
}

//before
<button onClick={() => this.props.dispatch(addTodo(this.state.input))}>

// after 
<button onClick={() => this.props.onAddTodo(this.state.input)}>

最終的なコード

import React, { Component } from 'react';
import { connect } from 'react-redux';
import { addTodo, removeTodo } from './actions/actions'
import './App.css';

class App extends Component {
  constructor() {
    super();
    this.state = {
      input: '',
    }
  }

  render () {
    const {onAddTodo, onRemoveTodo} = this.props;
    const {input} = this.state;
    return (
      <div className="App">
        <ul>
          {this.props.todos.map(todo => {
            return (
              <li key={todo}>
                <span>{todo}</span>
                <button onClick={() => onRemoveTodo(todo)}>
                  削除
                </button>
              </li>
            );
          })}
        </ul>
        <input 
          type="text"
          onChange={e => this.setState({input: e.target.value})}
        />
        <button onClick={() => onAddTodo(input)}>
          追加
        </button>
      </div>
    );
  }
}

const mapStateToProps = state => {
  return {
    todos: state.todos.list
  }
}



const mapDispatchToProps = dispatch => {
  return {
    onAddTodo(todo) {
      dispatch(addTodo(todo))
    },
    onRemoveTodo(todo) {
      dispatch(removeTodo(todo))
    }
  }
}

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

自分が学んだ事をまとめる事でただ写経をするより一層理解が深まったと思います。
個人的に苦戦したのはmapStateToProps, mapDispatchToPropsの部分でした。
上手く自分の中でデータフローが掴めず色んな箇所にconsole.logを入れてなんの値が入っているのかを確認したりしました笑
初めてReactを学習した時にも感じた事ですが、JavaScript(というよりES6)の文法がある程度分かっていないとサンプルのコードが読めず何の処理をしているのか分からなくなります。
React、ReduxはあくまでJavaScriptのフレームワーク・ライブラリであるのでまずはES6の基礎的な文法を理解してから望まないと辛いと思います。
特に分割代入やスプレッド演算子、プリミティブ型とオブジェクト型の違い…この辺を知っていないと学習していても??となるはずです。
逆に今あげた構文などを学習してからReact含めたFWなどに臨んでみると「おぉ!学んだ文法がここで使われてる!」みたいな気付きが生まれより一層記憶に残りやすくなると自分は感じました。

個人的におすすめなJavaScript学習サイト