抹桥的博客
Language
Home
Archive
About
GitHub
Language
主题色
250
2453 文字
12 分
【翻訳】Redux 入門:基礎編 (1)

Reduxの3つの原則#

  • アプリケーション全体のステートは、単一のstore内でオブジェクトツリーの形式で保持されます。
  • このオブジェクトツリーを変更する唯一の方法は、何が起こったかを記述するオブジェクトであるactionをディスパッチすることです。
  • 純粋な関数であるreducerを記述することで、そのactionがオブジェクトツリーをどのように変更するかを記述します。

Action#

Actionは、アプリケーションからstoreに送信される情報を運びます。これらはstoreの情報源にすぎません。store.dispatch() を介してactionを渡すことができます。 actionは次のような形をしています。

const ADD_TODO = 'ADD_TODO'
{
    type: ADD_TODO,
    text: 'Bulid my first Redux App'
}

Actionは通常のJavaScriptオブジェクトですが、どのような種類の操作が発生したかを指定するための type プロパティが必須です。type は文字列定数として定義されるべきです。プロジェクトが大規模になると、これらを個別のモジュールに移動する必要があるかもしれません。

import { ADD_TODO, REMOVE_TODO } from "../actionTypes";

type とは異なり、action全体の構造は完全にあなたが決定します。ただし、actionの構造をより良く整理する方法については、Flux Standard Action のガイドラインを参照してください。

Action Creators#

Action Creatorは、actionを生成するための関数です。 Reduxでは、action creatorは単純にactionオブジェクトを返すだけで十分です。

function addTodo(text) {
  return {
    type: ADD_TODO,
    text: "Some text",
  };
}

これにより、特定の種類のactionをより簡単に作成でき、テストも容易になります。

Dispatch#

実際にdispatchを初期化するには、結果を diapatch() 関数に渡すことで行えます。

store.dispatch(addTodo(text));

あるいは、自動的にdispatchするバインドされたaction creatorを作成することもできます。

const boundAddTodo = (text) => dispatch(addTodo(text));

これで、それらを直接呼び出すことができます。

boundAddTodo(text);

dispatch() 関数は store.dispatch() を介してstoreに直接アクセスできますが、react-reduxの connect() のようなヘルパー関数を使用してアクセスする方が好ましいかもしれません。bindActionCreators() を使用すると、多くのaction creatorを dispatch() 関数に自動的にバインドできます。

Reducers#

Actionは「何が起こったか」という事実を記述しますが、アプリケーションのstateをどのように変更するかは指定しません。これがreducerの役割です。

State構造の設計#

Reduxでは、アプリケーションのすべてのstateが単一のオブジェクトに保存されます。そのため、コードを書く前にstateをどのように設計するかを検討することが重要です。アプリケーションのstateをオブジェクトとして記述する最も簡単な方法を考えましょう。 todoアプリケーションの場合、2つの異なることを保存したいと考えます。

  • 現在選択されている表示フィルター条件
  • 実際のtodosのリスト

シンプルなstateは次のようになります。

{
   visibilityFilter: 'SHOW_ALL',
   todos: [
       {
           text: 'Consider using Redux',
           completed: true
       },{
           text: 'Keep all state in a single tree',
           completed: false
       }
   ]
}

Actionsの処理#

Reducerは純粋な関数であり、以前のstateとactionを引数として受け取り、次のstateを返します。

(previousState, action) => newState;

reducerと呼ばれるのは、Array.prototype.reduce(reducer,?initialValue) 関数に渡されるためです。そのため、reducerの純粋性を保つことが非常に重要です。reducer内で決して以下のことを行わないでください。

  • その関数引数を変更する
  • API呼び出しやルーティングの遷移など、副作用のある操作を行う
  • Date.now()Math.random() のような純粋でない関数を呼び出す

これらを理解した上で、reducer関数を始めましょう。初期stateを指定することから始めます。Reduxはreducerを最初に呼び出す際に undefined stateを渡します。このとき、初期化されたstateを返す必要があります。

import { VisibilityFilters } from "./actions";

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

function todoApp(state = initialState, action) {
  //采用 ES2015 写法,当 state 传递为 undefined 时,会被赋值为 initialState
  return state;
}

次に SET_VISIBILITY_FILTER を処理します。行う必要があるのは、stateの visibilityFilter を変更することです。

function todoApp(state = initialState, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return Object.assign({}, state, {
        visibilityFilter: action.SET_VISIBILITY_FILTER,
      });
    default:
      return state;
  }
}

注意すべき点:

  1. 元のstateを変更しません。Object.assign() を介して、元のstateと変更内容をマージしたコピーを作成します。
  2. 該当する状況が見つからない、つまりdefaultの場合には、以前のstateを返さなければなりません。

より多くのActionsの処理#

他にも処理すべきactionがいくつかあるので、それらもすべて追加します。

function todoApp(state = initialState, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return Object.assign({}, state, {
        visibilityFilter: action.filter,
      });
    case ADD_TODO:
      return Object.assign({}, state, {
        todos: [
          ...state.todos,
          {
            text: action.text,
            completed: false,
          },
        ],
      });
    default:
      return state;
  }
}

reducer関数を分離することで、より理解しやすくすることができます。todos関連の処理ロジックとvisibilityFilterの処理ロジックを一緒に置くのはあまり明確ではないからです。reducerの分離は非常に簡単です。

function todos(state = [], action) {
  switch (action.type) {
    case ADD_TODO:
      return [
        ...state,
        {
          text: action.text,
          completed: false,
        },
      ];
    default:
      return state;
  }
}

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

function todoApp(state = {}, action) {
  return {
    visibilityFilter: visibilityFilter(state.visibilityFilter, action),
    todos: todos(state.todos, action),
  };
}

ご覧の通り、各reducerはstate全体の自身の部分を管理しています。各reducerの state 引数は異なり、それぞれが管理する部分のstateに対応しています。 アプリケーションが大規模になると、reducerを複数の異なるファイルに分離し、独立性を保ちながら異なるデータソースを管理することができます。 最後に、Reduxは combineReducers() 関数を提供しており、上記のtodoAppと同様のロジックで複数のreducerを結合し、多くのボイラープレートコードを省略できます。

import { combineReducers } from "redux";

const todoApp = combineReducers({
  visibilityFilter,
  todos,
});

export default todoApp;

これは以下の記述と完全に等価です。

export default function todoApp(state = {}, action) {
  return {
    visibilityFilter: visibilityFilter(state.visibilityFilter, action),
    todos: todos(state.todos, action),
  };
}

combineReducers() が行っているのは、関数を生成し、各reducer関数に対応するstateを渡し、それらを単一のオブジェクトに結合することです。これは黒魔術ではありません。

combineReducers 原理#

2015年にReduxを見たとき、ドキュメントを読んでいなかったので、combineReducers() が実際に何をしているのか理解できず、黒魔術だと思っていました。 さらに、combineReducers() 後の関数を介して初期化されたstoreを生成することもできます。

今日、ドキュメントと黒魔術に関するこのissueを読み直して理解しました。 実際、combineReducers() はこのように機能します。todoリストを管理するreducerと、現在選択されているフィルター状態を管理するreducerの2つがあると仮定します。

 function todos(state = [], action) {
   // Somehow calculate it...
   return nextState
 }
function visibleTodoFilter(state = 'SHOW_ALL', action) {
   // Somehow calculate it...
   return nextState
 }
let todoApp = combineReducers({
   todos,
   visibleTodoFilter
 })

ご覧の通り、各reducerにはデフォルトのstateが定義されています。todos では []visibleTodoFilter では SHOW_ALL です。 actionがトリガーされると、combineReducers によって返された todoApp全体のreducerを呼び出します。

let nextTodos = todos(state.todos, action);
let nextVisibleTodoFilter = visibleTodoFilter(state.visibleTodoFilter, action);

最終的に、各reducerが返すstateを単一のstateツリーに結合します。

return {
  todos: nextTodos,
  visibleTodoFilter: nextVisibleTodoFilter,
};

これで combineReducers の動作原理を理解できます。もちろん、combineReducers を使用しない選択も可能です。結局のところ、これは公式が提供するヘルパーツールに過ぎず、独自のルートreducerを自分で実装することもできます。

Store#

Storeは、それらを連携させるためのオブジェクトです。Storeの役割は以下の通りです。

  • アプリケーション全体のstateを保存する
  • getState() を介してstateを取得できる
  • dispatch(action) を介してstateを更新できる
  • subscribe(listener) を介してリスナーを登録する
  • subscribe(listener) が返す値を使って、登録解除されたリスナーを処理する

Reduxアプリケーションにはstoreが1つしかないことに注意が必要です。データロジックを分離したい場合は、より多くのstoreを作成する代わりに、より多くのreducerを作成することで実現できます。 reducerが1つあれば、storeの作成は簡単です。combineReducers() で作成したルートreducerを介してstoreを作成できます。

import { createStore } from "redux";

import todoApp from "./reducers";

let store = createStore(todoApp);

オプションの引数を渡してstateを初期化することもできます。これは、ユニバーサルアプリケーションを開発する際に非常に役立ち、サーバーから渡されたstateをクライアントの初期stateとして使用できます。

let store = createStore(todoApp, window.STATE_FROM_SERVER);

总结#

これらの内容を理解すれば、Reduxがどのように機能するのかおおよそ分かるでしょう。ドキュメントを読むことはやはり非常に役立ちます。昨年、公式のサンプルコードを直接見て、結局何が何だか分からなかったのとは違います。 次に、データフローとReactとの連携について学び続け、理解を深めるためにTodoListのインスタンスを実際に作成します。

参考#

Basics|Redux

この記事は 2016年4月27日 に公開され、2016年4月27日 に最終更新されました。3449 日が経過しており、内容が古くなっている可能性があります。

【翻訳】Redux 入門:基礎編 (1)
https://blog.kisnows.com/ja-JP/2016/04/27/step-to-redux-1/
作者
Kisnows
公開日
2016-04-27
ライセンス
CC BY-NC-ND 4.0