Reactと使う

まず最初に、ReduxはReactと何の関係もないことを強調する必要があります。ReduxアプリはReact、Angular、Ember、jQuery、あるいはJavaScript単体とでも書けます。

とはいえ、ReduxはReactDekuのようなライブラリと特に相性が良いです。これらのライブラリではUIを状態の関数として表すことができ、ReduxはActionに反応して状態更新を送り出すからです。

ここではReactを、シンプルなTodoアプリを作るのに使いましょう。

React Reduxをインストール

デフォルトでは、React bindingsはReduxに含まれていません。明示的にインスールする必要があります:

npm install --save react-redux

npmを使っていない方は、unpkgから最新のUMDビルドを簡単に利用しても良いです(開発(development)本番(production)ビルドのどちらか)。<script>でページに加えると、window.ReactReduxというグローバル変数がエクスポートされます。

プレゼンテーショナルとコンテナ、2種類のコンポーネント

Redux用のReactバインディング(連携プログラム)は、プレゼンテーショナルとコンテナという、2種類のコンポーネント(構成要素)を分離する というアイデアを取り入れています。まずこれを読んで(read about them first)から、戻ってきてください。重要なことが書かれているので、待っています!

記事を読み終わりましたか? 2つの違いをおさらいしましょう:

プレゼンテーショナルコンポーネント コンテナコンポーネント
目的 どう見えるか(マークアップ、スタイル) どう働くか(データ取得、状態更新)
Reduxの存在 知らない 知っている
データ読み込み Propsから読み込む Reduxの状態を購読する
データ変更 Propsからコールバックを呼び出す ReduxのアクションをDispatch(送信)する
作り方 手作業で書く 通常はReact Reduxで生成する

書くことになるコンポーネントのほとんどは、プレゼンテーショナルでしょう。しかしReduxのStoreとつなげるために、コンテナコンポーネントもいくつか生成する必要があります。この事と下記のデザイン概要は、コンテナコンポーネントが必ずコンポーネントツリーの上位にいなければならないと言っているのではありません。

というのも、コンテナコンポーネントが複雑になりすぎる場合があります。深くネストされたプレゼンテーショナルコンポーネントに、無数のコールバックを伝えるような場合です。このようなときは、コンポーネントツリー内にもう1つコンテナコンポーネントを導入してください。FAQでも言及しています。

技術的に、コンテナコンポーネントを手作業で書くことは可能です。この場合、store.subscribe()を使います。しかし、おすすめしません。なぜならReact Reduxは、多くのパフォーマンス最適化を行なっているからです。これを手で書くのは難しいです。このためコンテナコンポーネントは書かずに、React Reduxで用意されているconnect()関数で生成しましょう。下記で確認できます。

コンポーネント階層をデザインする

どのようにルート(大元の)状態オブジェクトの形をデザインしたか、覚えていますか? 状態オブジェクトの形に合わせて、UI階層をデザインします。これはRedux特有の作業ではありません。Reactで考えること(Thinking in React)は、このプロセスを説明する素晴らしいチュートリアルです。

Todoアプリのデザイン概要はシンプルです。Todo項目のリストを表示したい。クリックで、個々のTodoは完了済みにする。ユーザーが新しいTodo項目を追加する欄を表示したい。すべてのTodo、完了のみ、未完のみを選択表示するために、フッターにトグル(切り替え)を表示したい。

プレゼンテーショナルコンポーネントをデザインする

プレゼンテーショナルコンポーネントとそのPropsは、次の概要から浮かび上がってきます:

  • TodoList はTodoを表示するリスト。
    • todos: Array はTodo項目の配列。項目の形は{ id, text, completed }
    • onTodoClick(id: number) はコールバック。Todo項目をクリックすると呼び出される。
  • Todo は1つのTodo項目。
    • text: string は表示するテキスト。
    • completed: boolean はTodo項目が完了済みかどうか。
    • onClick() はコールバック。Todo項目をクリックすると呼び出される。
  • Link はコールバックが紐づいたリンク。
    • onClick() はコールバック。リンクをクリックすると呼び出される。
  • Footer はユーザーが、現在の表示項目を変更できる場所。
  • App はルートコンポーネント。その他すべてを描画する。

これらは 見た目 を説明しています。しかし どこから データが来るか、また どうやって データを変化させるかは知りません。ただ与えられたものを描画するだけです。Reduxから何か他へ移行するなら、これらのコンポーネントをまったく同じように保持できます。Reduxへの依存はありません。

コンテナコンポーネントをデザインする

プレゼンテーショナルコンポーネントをReduxにつなげるため、コンテナコンポーネントも必要です。例えばプレゼンテーショナルコンポーネントのTodoListは、VisibleTodoListのようなコンテナコンポーネントが必要です。ReduxのStoreを購読して、現在の表示フィルターをどう適用するか把握するためです。表示フィルターを変更するためには、FilterLinkというコンテナコンポーネントを用意しましょう。プレゼンテーショナルコンポーネントのLinkを描画し、クリックに応じて適切なActionをDispatchするためです:

  • VisibleTodoList は現在の表示フィルターに従って、Todo項目を選別します。そしてTodoListを描画します。
  • FilterLink は現在の表示フィルターを取得して、Linkを描画します。
    • filter: string はそれぞれの表示フィルター(すべてのTodo、完了のみ、未完のみ)を表しています。

他のコンポーネントをデザインする

あるコンポーネントをプレゼンテーショナルとコンテナコンポーネントのどちらにすべきか、判断しづらいことがあります。例えば、フォームと関数が結びついているような場合です。次の小さなコンポーネントのように:

  • AddTodo は“追加”ボタンのついた入力欄です。

技術的には、2つのコンポーネントに分割できます。しかしこの段階では早すぎるかもしれません。とても小さなコンポーネントでは、プレゼンテーション(表現)とロジック(論理)を一緒にしても問題ありません。アプリが大きくなるにつれて、分割のやり方はより明らかになります。そのため今は、一緒にしておきましょう。

コンポーネントを実装する

コンポーネントを書きましょう!まずはプレゼンテーショナルコンポーネントからです。従って、まだReduxへのバインディングを考える必要はありません。

プレゼンテーショナルコンポーネントを実装する

これらはすべて、普通のReactコンポーネントです。そのため詳しくは説明しません。ローカルの状態やライフサイクルメソッドを使う必要がない限り、状態を持たない関数としてコンポーネントを書きます。これはプレゼンテーショナルコンポーネントが関数で なければならない という意味ではありません。ただ定義するのが、より簡単になります。関数として定義したコンポーネントは、クラスに書き換えることができます。書き換えるのは、ローカルの状態やライフサイクルメソッドを追加する必要のあるときや、パフォーマンスを最適化しなければならないときです。

components/Todo.js

import React from 'react'
import PropTypes from 'prop-types'

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

Todo.propTypes = {
  onClick: PropTypes.func.isRequired,
  completed: PropTypes.bool.isRequired,
  text: PropTypes.string.isRequired
}

export default Todo

components/TodoList.js

import React from 'react'
import PropTypes from 'prop-types'
import Todo from './Todo'

const TodoList = ({ todos, onTodoClick }) => (
  <ul>
    {todos.map((todo, index) => (
      <Todo key={index} {...todo} onClick={() => onTodoClick(index)} />
    ))}
  </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

components/Link.js

import React from 'react'
import PropTypes from 'prop-types'

const Link = ({ active, children, onClick }) => {
  if (active) {
    return <span>{children}</span>
  }

  return (
    <a
      href=""
      onClick={e => {
        e.preventDefault()
        onClick()
      }}
    >
      {children}
    </a>
  )
}

Link.propTypes = {
  active: PropTypes.bool.isRequired,
  children: PropTypes.node.isRequired,
  onClick: PropTypes.func.isRequired
}

export default Link

components/Footer.js

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

コンテナコンポーネントを実装する

次に、いくつかコンテナコンポーネントを作ります。上記のプレゼンテーショナルコンポーネントをReduxにつなぐためです。技術的に、コンテナはただのReactコンポーネントです。store.subscribe()を使い、Reduxの状態ツリーの一部を読み込みます。そしてPropsを、描画するプレゼンテーショナルコンポーネントに渡します。

コンテナコンポーネントは手作業で書けます。しかし、React Reduxライブラリのconnect()関数で生成することをおすすめします。このライブラリには、多くの役立つ最適化が用意されています。不必要な再描画を避けるためです。 (これによる1つの成果があります。それは、shouldComponentUpdateを自分で実装するというReactのパフォーマンス提案(React performance suggestion)について、心配しなくて良いということです)

connect()を使うためには、mapStateToPropsという特別な関数を定義する必要があります。この関数は現在のRedux Storeの状態を、どのようにPropsへ変換するかを示します。このPropsは、コンテナがラップ(内包)しているプレゼンテーショナルコンポーネントに渡されます。例えば、VisibleTodoListは状態のtodosを計算しなければいけません。計算したtodosは、TodoListに渡します。そのために、state.visibilityFilterに従ってstate.todosを選別する関数を定義します。そしてこの関数を、mapStateToProps内で使います:

const getVisibleTodos = (todos, filter) => {
  switch (filter) {
    case 'SHOW_COMPLETED':
      return todos.filter(t => t.completed)
    case 'SHOW_ACTIVE':
      return todos.filter(t => !t.completed)
    case 'SHOW_ALL':
    default:
      return todos
  }
}

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

状態の読み込みに加えて、コンテナコンポーネントはActionのDispatchもできます。同じようにして、mapDispatchToProps()という関数を定義します。この関数はdispatch()メソッドを受け取り、 コールバックとなるPropsを返します。このコールバックを、プレゼンテーショナルコンポーネントに渡すためです。例えばVisibleTodoListからTodoListコンポーネントへ、onTodoClickというコールバックをPropとして渡します。そしてonTodoClickから、TOGGLE_TODOというActionをDispatchします:

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

最後に、VisibleTodoListを作ります。そのためにconnect()を呼び出し、上記2つの関数を渡します:

import { connect } from 'react-redux'

const VisibleTodoList = connect(
  mapStateToProps,
  mapDispatchToProps
)(TodoList)

export default VisibleTodoList

これらはReact Redux APIの基本です。しかしもっと手っ取り早い方法や、有力な選択肢がいくつかあります。そのためこのドキュメント(its documentation)を詳しく調べてみると良いでしょう。mapStateToPropsが新しいオブジェクトを頻繁に作りすぎていると心配になるときは、reselectとともに派生データの計算を学ぶと良いかもしれません。

残りのコンテナコンポーネントを、下記で定義します:

containers/FilterLink.js

import { connect } from 'react-redux'
import { setVisibilityFilter } from '../actions'
import Link from '../components/Link'

const mapStateToProps = (state, ownProps) => {
  return {
    active: ownProps.filter === state.visibilityFilter
  }
}

const mapDispatchToProps = (dispatch, ownProps) => {
  return {
    onClick: () => {
      dispatch(setVisibilityFilter(ownProps.filter))
    }
  }
}

const FilterLink = connect(
  mapStateToProps,
  mapDispatchToProps
)(Link)

export default FilterLink

containers/VisibleTodoList.js

import { connect } from 'react-redux'
import { toggleTodo } from '../actions'
import TodoList from '../components/TodoList'

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)
  }
}

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

const VisibleTodoList = connect(
  mapStateToProps,
  mapDispatchToProps
)(TodoList)

export default VisibleTodoList

他のコンポーネントを実装する

containers/AddTodo.js

先ほど言ったように、AddTodoコンポーネントではプレゼンテーションとロジックが一緒に定義されています。

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>
  )
}
AddTodo = connect()(AddTodo)

export default AddTodo

もしref属性に不慣れなら、このドキュメントを読んでください。ref属性の推奨される使い方が分かります。

1つのコンポーネント内で、複数のコンテナをまとめる

components/App.js

import React from 'react'
import Footer from './Footer'
import AddTodo from '../containers/AddTodo'
import VisibleTodoList from '../containers/VisibleTodoList'

const App = () => (
  <div>
    <AddTodo />
    <VisibleTodoList />
    <Footer />
  </div>
)

export default App

Storeを渡す

すべてのコンテナコンポーネントは、Redux Storeにアクセスする必要があります。Storeを購読するためです。購読するための1つの方法は、すべてのコンテナコンポーネントにPropとしてStoreを渡すことでしょう。でもこれは面倒です。なぜならstoreを、プレゼンテーショナルコンポーネントにまで渡す必要があるからです。たまたまプレゼンテーショナルコンポーネントが、コンポーネントツリーの深いところでコンテナを描画するというだけの理由で。

おすすめの方法は、<Provider>という特別なReact Reduxコンポーネントを使うことです。このコンポーネントは、魔法のように(magically)アプリケーション内のすべてのコンテナコンポーネントでStoreを利用できるようにします。明示的にStoreを渡さなくて良いのです。ルートコンポーネントを描画するときに1度、このコンポーネントを使うだけです:

index.js

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

let store = createStore(todoApp)

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

次のステップ

このチュートリアルの完全なソースコードを読んでください。習得した知識を、より深く自分のものにするためです。そして、上級チュートリアルへ直行しましょう。ネットワークリクエストとルーティングの処理方法を学ぶためです!

results matching ""

    powered by

    No results matching ""