抹桥的博客
Language
Home
Archive
About
GitHub
Language
主题色
250
3303 文字
17 分
【翻訳】Redux の基礎編 (2)

前回のRedux入門:基礎編(一)に引き続き、今回は公式ドキュメントに沿って、以下の3つの点から解説していきます。

  • データフロー
  • Reactとの連携
  • 実例:TodoList

データフロー#

Reduxのアーキテクチャは、厳格な単方向データフローを構築する方法を中心に設計されています。

これは、すべてのデータが同じライフサイクルパターンに従うことを意味し、アプリケーション全体を予測可能で理解しやすくします。Reduxアプリケーションにおけるデータのライフサイクルは、以下の4つのステップに分けられます。

1 あなたが store.dispatch(action) を能動的に呼び出す#

Action は、何が起こったかを記述するオブジェクトです。例えば:

{type:'ADD_TODO',text:'todo\'s content'}
{type: 'FETCH_USER_SUCCESS', response: {id: 3, name: 'Mary'}}

Action は、イベントの短い断片的な説明と考えることができます。store.dispatch(action) は、コンポーネントやXHR内、さらにはタイマー内など、どこからでも呼び出すことができます。

2 Redux store があなたが提供する reducer 関数を呼び出す#

store は reducer に2つの引数を渡します:現在の state と action です。例えば、Todoアプリケーションでは、ルート reducer は以下のような引数を受け取ります:

let previousState = {
  visibleTodoFilter: "SHOW_ALL",
  todos: [
    {
      text: "Read the docs",
      complete: false,
    },
  ],
};
let action = {
  type: "ADD_TODO",
  text: "Understand the flow",
};
// nextState 由 todoApp 这个根 reducer 来生成
let nextState = todoApp(previousState, action);

注意すべきは、reducer が純粋関数であり、副作用がなく、次の state を計算するだけであるということです。同じ引数に対しては、何度呼び出しても常に同じ値を返す、完全に予測可能なものであるべきです。API呼び出しやルーティングの変更などは、action が dispatch される前に行われるべきです。

3 ルート reducer は複数の子 reducer の出力を結合して単一の state ツリーを生成する#

ルート reducer をどのように組み合わせるかは完全にあなた次第です。Redux は combineReducers() というヘルパー関数を提供しており、ルート reducer を独立した関数に分割して state ツリーの特定の部分を管理する際に非常に役立ちます。 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
 })

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 を使用しない選択肢もあります。結局のところ、それは便利な補助ツールに過ぎず、独自のルート reducer を実装することも可能です。

4 Redux store がルート reducer によって返された state ツリー全体を保存する#

この新しい state ツリーが、現在のアプリケーションの次の state となります。store.subscribe(listener) を通じて登録されたすべてのリスナー関数が呼び出され、リスナーオブジェクトは store.getState() を呼び出して現在の state を取得することができます。 これで、ビュー全体が新しい state を使って更新されます。React Redux をビューバインディングツールとして使用している場合、これは component.setState(newState) が呼び出されるポイントです。

Reactとの連携#

Redux と React の間には直接的な関連性はありませんが、Redux は React や Deku のようにデータ状態によってUIを記述できるフレームワークと非常に相性が良いです。ここでは、React を使用してシンプルな Todo アプリケーションを構築します。

React Redux のインストール#

React バインディングは Redux にデフォルトで含まれていないため、別途インストールする必要があります:

npm install --save react-redux

プレゼンテーショナルコンポーネントとコンテナコンポーネント#

Redux の React バインディングは、プレゼンテーショナルコンポーネントとコンテナコンポーネントの分離という考え方を支持しています。

項目プレゼンテーショナルコンポーネントコンテナコンポーネント
目的UIの見え方(マークアップ、スタイル)機能の動作(データ取得、状態更新)
Reduxを意識するかいいえはい
データの読み取り親のpropsから読み取るRedux stateを購読して取得する
データの変更propsからコールバック関数を呼び出すRedux actionsをdispatchする
記述方法手動で記述通常はReact Reduxによって生成される

ほとんどのコンポーネントはプレゼンテーショナルコンポーネントとして記述されるべきですが、それらをRedux storeに接続するためのコンテナコンポーネントクラスもいくつか生成する必要があります。

技術的には、store.subscribe() を使用してコンテナコンポーネントを手動で記述することも可能です。しかし、React Redux は手動では実装が難しい多くのパフォーマンス最適化を行っているため、これは推奨されません。したがって、手動でコンテナコンポーネントを記述するよりも、React Redux が提供する connect() 関数を使って生成することをお勧めします。

コンポーネント階層の設計#

私たちの設計は非常にシンプルです。Todoアイテムのリストを表示したいと考えています。Todoアイテムが完了したかどうかをマークするためのボタン。新しいTodoを追加する場所。フッターには、すべてのTodo、完了したTodo、未完了のTodoを表示するための切り替えボタンが必要です。

プレゼンテーショナルコンポーネント#

以下のプレゼンテーショナルコンポーネントを通じて、propsの階層を概説できます。

  • TodoList は利用可能なTodoのリストを表示します。
    • todos: Array { id, text, completed } の形式のリスト
    • onTodoClick(id: number) Todoがクリックされたときのコールバック関数
  • Todo は個々のTodoアイテムです。
    • text: string 表示するテキスト
    • completed: boolean Todoが完了したかどうかの状態
    • onClick Todoがクリックされたときに呼び出されるコールバック関数
  • Link はコールバックを持つリンクです。
    • onClick このリンクがクリックされたときのコールバック
  • Footer は現在表示されているTodoの内容を変更する場所です。
  • App はルートコンポーネントで、他のすべてをレンダリングします。

これらはアプリケーションがどのように見えるかを記述しますが、データがどこから来るのか、どのように変更するのかは知りません。ただ与えられたデータをレンダリングするだけです。Reduxから他のフレームワークに移行する場合でも、これらのコンポーネントはほとんど変更せずに済みます。これらはReduxとは関連がありません。

コンテナコンポーネント#

同様に、プレゼンテーショナルコンポーネントをReduxに接続するためのコンテナコンポーネントもいくつか必要です。例えば、TodoList コンポーネントは、現在の可視性フィルター(visibility filter)をどのように使用するかを知るために、Reduxからデータを購読する VisibleTodoList というコンテナを必要とします。可視性フィルターを変更するために、クリック時に適切なactionをdispatchする Link をレンダリングする FilterLink コンテナを提供します:

  • VisibleTodoList は現在の可視性フィルターに基づいて表示する内容をフィルタリングし、TodoList をレンダリングします。
  • FilterLink は現在の可視性フィルターを取得し、Link をレンダリングします。
    • filter: string 可視性フィルターを表す文字列

その他のコンポーネント#

時には、コンポーネントがプレゼンテーショナル型かコンテナ型かを区別するのが難しい場合があります。例えば、フォームと関数が相互に依存している場合など、この小さなコンポーネントのように:

  • AddTodo は「追加」ボタン付きの入力ボックスです。

技術的には、これを2つのコンポーネントに分離することも可能ですが、それでは明らかに煩雑すぎます。プロジェクトが複雑で大規模になったときに分離することはできますが、今のところは、これらを混合したままにしておきましょう。

コンポーネントの実装#

プレゼンテーショナルコンポーネント#

これらは通常のReactコンポーネントなので、詳細な説明は省略します。

TodoList.js
import React, { PropTypes } from "react";

import Todo from "./Todo";

const TodoList = ({ todos, onTodoClick }) => (
  <ul>
    {todos.map((todo) => (
      <Todo key={todo.id} {...todo} onClick={() => onTodoClick(todo.id)} />
    ))}
  </ul>
);
TodoList.propTypes = {
  todos: PropTypes.arrayOf(
    PropTypes.shape({
      id: PropTypes.number.isRequired,
      completed: PropTypes.bool.isRequired,
      text: PropTypes.string.isRequired,
    }).isRequired,
  ).isRequired,
  onTodoClick: PropTypes.func.isRequired,
};
export default TodoList;

コンテナコンポーネント#

次に、コンテナコンポーネントを生成して、プレゼンテーショナルコンポーネントとReduxを連携させます。技術的には、コンテナコンポーネントは、store.subscribe() を使用してRedux stateツリーの一部を読み取り、レンダリングのためにプレゼンテーショナルコンポーネントにpropsを提供する単なるReactコンポーネントです。このコンポーネントを手動で記述することもできますが、Reduxが提供する connect() 関数を使用してこれらのコンポーネントを生成することをお勧めします。これは、不要な再レンダリングを防ぐための多くの有用な最適化を提供します。

connect() を使用するには、現在のRedux store state をプレゼンテーショナルコンポーネントに渡したいpropsに変換する方法を指定する mapStateToProps という特別な関数を定義する必要があります。例えば、VisibleTodoListTodoList に渡す todos を計算する必要があるため、state.visibilityFilter を使って state.todos をフィルタリングする関数を定義し、それを mapStateToProps で使用します:

const getVisibleTodos = (todos, filter) => {
  switch (filter) {
    case "SHOW_ALL":
      return todos;
    case "SHOW_COMPLETED":
      return todos.filter((t) => t.completed);
    case "SHOW_ACTIVE":
      return todos.filter((t) => !t.completed);
  }
};
const mapStateToProps = (state) => {
  return {
    todos: getVisibleTodos(state.todos, state.visibilityFilter),
  };
};

state を読み取るだけでなく、コンテナコンポーネントはactionをdispatchすることもできます。同様に、dispatch() メソッドを受け取り、目的のプレゼンテーショナルコンポーネントに注入するコールバックpropsを返す mapDispatchToProps() という関数を定義できます。例えば、VisibleTodoListTodoList コンポーネネントに onTodoClick というpropを注入し、onTodoClickTOGGLE_TODO action をdispatchするようにしたい場合:

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

最後に、connect() を呼び出して VisibleTodoList を生成し、これら2つの関数を渡します:

import { connect } from "react-redux";

const VisibleTodoList = connect(mapStateToProps, mapDispatchToProps)(TodoList);
export default VisibleTodoList;

これらはReact Reduxの基本的なAPIですが、いくつかのショートカットや強力なオプションがあるため、こちらのドキュメントを注意深く確認することをお勧めします。mapStateToProps が新しいオブジェクトを作成するプロセスについて心配な場合は、reselect を使用した派生データの計算について学ぶ必要があるかもしれません。

Store への受け渡し#

すべてのコンテナコンポーネントは、Redux store に接続して購読する必要があります。一つの方法は、それを各コンテナコンポーネントにpropとして渡すことですが、それは煩雑すぎます。 私たちが推奨する方法は、特定のReact Reduxコンポーネント <Provider> を使用して、store を明示的に渡すことなく、アプリケーション内のすべてのコンテナコンポーネントで利用できるようにする「魔法」を使うことです。ルートコンポーネントをレンダリングする際に一度だけ呼び出すだけで済みます。

Index.js
import React from "react";
import { render } from "react-dom";
import { Provider } from "react-redux";
import { createStore } from "redux";

import App from "./components/App";
import todoApp from "./reducers";

let store = createStore(todoApp);
render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById("root"),
);

実例:TodoList#

ドキュメントを直接読むだけでは、多くの専門用語やメソッドを知っても、全体像を把握するのは難しいでしょう。やはりコードを見る必要があります。ここに公式のTodoListのソースコードがありますので、これら2つの記事と合わせて読めば、よく理解できるはずです。 ここでいくつかの問題に遭遇しました。

  1. webpack が常にエントリファイル ’./index’ を見つけられないとエラーを吐き続け、最終的にbabelの落とし穴を発見しました。Babelが6.0にアップグレードされてから、コンパイルするjsファイルのタイプを .babelrc ファイルで指定する必要があり、そうしないとコンパイルエラーが発生します。

    {
    "presets": ["es2015", "react"]
    }
  2. ページがレンダリングされた後、追加などの操作は正常に機能しました。しかし、フィルターをクリックすると常にエラーが発生し、そのエラーも不可解なものでした。そこで、一つずつブレークポイントを置いて確認したところ、最終的に Footer.js 内のフィルターに対応するactionのスペルミスが原因で、後続のコンポーネントが対応するactionを見つけられないことが判明しました。

    import React from "react";
    
    import FilterLink from "../containers/FilterLink";
    
    const Footer = () => (
      <p>
        Show: <FilterLink filter="SHOW_ALL">All</FilterLink>
        {", "}
        <FilterLink filter="SHOW_ACTIVE">Active</FilterLink>
        {", "}
        <FilterLink filter="SHOW_COMPLETED">Completed</FilterLink>
      </p>
    );
    export default Footer;

    このことから、アプリケーションのactionはやはり専用のファイルで個別に管理し、すべてのactionを変数として定義してエクスポートする必要があることがわかります。そうすれば、後でスペルミスがあった場合でも、コンパイル段階で問題を発見できます。

参考#

Basics|Redux

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

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