banner
他山之石

他山之石

react-reduxを使ってシンプルなTo doアプリを開発する

redux は状態管理に特化し、react と分離されています。使いやすさのために、redux の作者は react 専用のライブラリ react-redux をラップしました。

image

前回はシンプルなカウンターアプリを通じて redux の基本原理と使い方を学びました。本記事では、redux がどのように react と組み合わせて使用されるかを紹介します。使いやすさのために、redux の作者は react 専用のライブラリ react-redux をラップしました。これが今後の焦点です。

本文の完全なコードは Github でご覧ください:https://github.com/YanYuanFE/redux-app

// リポジトリをクローン
git clone https://github.com/YanYuanFE/redux-app.git


cd redux-app

// ブランチをチェックアウト
git checkout part-2

// インストール
npm install

// スタート
npm start

インストール#

npm install react-redux --save

基本 API#

コンポーネント#

react-redux は Provider コンポーネントを提供し、ルートコンポーネントの最外層にラップして、store をコンポーネント内部に渡します。これにより、アプリ内部の任意の子孫コンポーネントが非常に簡単にグローバルな状態を取得できます。コアの原理は react が提供する context API です。
サンプルコードは以下の通りです:

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import './index.css';
import App from './App';
import { createStore } from 'redux';
import reducer from './reducers';

const store = createStore(reducer);

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

connect()#

react-redux は connect メソッドを提供し、react コンポーネントと redux の store を接続します。connect メソッドを使用することで、任意の react コンポーネント内で store のグローバル state と Action Creator をコンポーネントの props に渡すことができます。

connect メソッドは 2 つの引数、mapStateToPropsmapDispatchToPropsを受け取り、コンポーネントの入力と出力を定義します。mapStateToProps は入力ロジックを担当し、state をコンポーネントの props にマッピングします。後者は出力ロジックを担当し、Action Creator メソッドをコンポーネントの props に渡します。コンポーネント内で Action を発動できます。
サンプルコードは以下の通りです:

import { connect } from 'react-redux';

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

export default FilterLink;

mapStateToProps#

mapStateToProps は関数で、2 つの引数、state と props を受け取ります。state は全体のグローバルオブジェクトツリーを含み、props はコンポーネントの props を表します。一般的には state の使用が最も多いです。実行後、オブジェクトを返します。オブジェクトのキーと値はマッピングを表します。
サンプルコードは以下の通りです:

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

上記のコードでは、mapStateToProps 内でグローバル state とコンポーネントの props を使用して計算し、キー active を含むオブジェクトを得て、コンポーネントに渡します。コンポーネント内では this.props.active を通じて値を取得できます。

mapDispatchToProps#

mapDispatch は関数またはオブジェクトで、store.dispatch をコンポーネントの props にマッピングします。mapDispatchToProps が関数の場合、dispatch と ownProps の 2 つの引数を受け取り、オブジェクトを返します。オブジェクトのキーと値は、コンポーネントの props に渡すメソッドと対応する action を定義します。
サンプルコードは以下の通りです:

const mapDispatchToProps = (
  dispatch,
  ownProps
) => {
  return {
    onClick: () => {
      dispatch({
        type: 'SET_VISIBILITY_FILTER',
        filter: ownProps.filter
      });
    }
  };
}

上記のコードは dispatch メソッドをコンポーネントの props パラメータ onClick にマッピングし、コンポーネント内部で this.props.onClick()を通じて呼び出すことができます。

mapDispatch がオブジェクトの場合、そのキーと値はそれぞれコンポーネントの props パラメータと Action Creator の関数になります。通常、大規模なアプリケーションでは、actionCreators ファイルを専用のファイルに定義します。以下のように:

export const setVisibilityFilter = (filter) => ({
  type: 'SET_VISIBILITY_FILTER',
  filter
})

上記のコードは一連の actionCreator の一つで、Action を返し、Redux が自動的に発行します。上記のコードは以下のように使用されます:

import { setVisibilityFilter } from '../actions';

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

この方法は関数として書く方法と実際には同じで、action を個別に抽出することで、冗長なコードを避けることができます。複数のコンポーネントで同じ action を発動する必要がある場合に便利です。

実践 To do アプリ#

前回は react-redux のコア API を理解しました。次に、完全なアプリを完成させます。

アプリ全体の実装画面は以下の通りです:

image

主な機能は、テキストを入力して todo 項目を追加し、todo リストを表示し、todo 項目をクリックして状態を切り替え、タブをクリックして異なる todo 状態をフィルタリングすることです。

アプリ開発の過程で、components を使用します。まず、create-react-app を使用して react アプリを初期化し、src が主要な開発ディレクトリです。src ディレクトリ内に、actions、components、containers、reducers フォルダをそれぞれ新規作成します。actions は actionCreator を作成するために使用され、reducers フォルダ内はアプリケーションの reducer を作成するために使用されます。component フォルダ内では UI コンポーネントを作成します。これは無状態コンポーネントで、UI の表示のみを担当し、ビジネスロジックを担当せず、データは props を通じて渡され、redux 関連の API を操作しません。containers フォルダ内では、データとビジネスロジックを管理し、redux の API を操作するコンテナコンポーネントを作成します。

まず、App.jsにはアプリ全体の UI インターフェースが含まれています。

import React, { Component } from 'react';
import Footer from './components/Footer';
import AddTodo from './containers/AddTodo';
import VisibleTodoList from './containers/VisibleTodoList';
import logo from './logo.svg';
import './App.css';

class App extends Component {
  render() {

    return (
      <div className="App">
        <div className="todoapp">
          <AddTodo/>
          <VisibleTodoList/>
          <Footer/>
        </div>
      </div>
    );
  }
}

export default App;

アプリ全体には 3 つのコンポーネントAddTodoVisibleTodoListFooterが含まれています。

次に、全体のインターフェースの実装を段階的に紹介します。
containers フォルダから始めます。まずは AddTodo コンポーネントで、todo を追加するためのものです。

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">
          Todoを追加
        </button>
      </form>
    </div>
  )
}
AddTodo = connect()(AddTodo)

export default AddTodo

AddTodo は Form 表単を含み、Form 表単の onSubmit イベント内で入力値を取得し、action に送信します。全体のコンポーネントは redux の connect コンポーネントでラップされており、dispatch がコンポーネントの props に渡されます。

次に actions の addTodo メソッドです。

let nextTodoId = 0
export const addTodo = (text) => ({
  type: 'ADD_TODO',
  id: nextTodoId++,
  text
})

addTodo は新しい Todo を作成し、todo の id を 1 増やし、todo の text 属性を渡します。

次に VisibleTodoList コンポーネントです。現在選択されているタブに基づいて異なる状態の todo リストを表示します。

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)
    default:
      throw new Error('Unknown filter: ' + filter)
  }
}

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

const mapDispatchToProps = {
  onTodoClick: toggleTodo
}

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

export default VisibleTodoList

コードからわかるように、VisibleTodoList は UI に関連する部分を持たず、TodoList コンポーネントをラップして新しいコンポーネントを返すだけです。mapStateToProps では、現在のすべての todos とフィルタ条件 visibilityFilter に基づいて、getVisibleTodos 関数を通じてフィルタリングされた todos を返します。mapDispatchToProps は actionCreator メソッドを UI コンポーネントに props として渡します。

actions フォルダ内の index.js では、toggleTodo を以下のように定義しています:

export const toggleTodo = (id) => ({
  type: 'TOGGLE_TODO',
  id
})

toggleTodo は渡された id を使用して現在操作中の todo を処理します。

次に components フォルダ内の TodoList コンポーネントです。todo リストを表示する役割を担っています。

import React from 'react'
import PropTypes from 'prop-types'
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

上記のコードからわかるように、TodoList は props から todos と onTodoClick を受け取り、todos をレンダリングします。各 todo は別の Todo コンポーネントに抽出され、各 Todo にクリックイベントを渡します。クリックイベント内で onTodoClick を呼び出し、現在の todo の id を渡して todo の状態を変更します。

Todo コンポーネントは以下の通りです:

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

TodoList コンポーネント内で、すべての todo リストをレンダリングする際に、各 todo をオブジェクト解構し、Todo コンポーネントの props に渡します。Todo コンポーネント内で、必要なパラメータ onClick、completed、text を取得します。onClick は todo のクリックイベントにバインドされ、completed は todo の状態が完了しているかどうかを示し、completed の値に基づいて todo のスタイルをレンダリングし、text は todo の内容を表示します。
次に components フォルダ内の Footer コンポーネントです:

import React from 'react'
import FilterLink from '../containers/FilterLink'

const Footer = () => (
  <p>
    表示:
    {" "}
    <FilterLink filter="SHOW_ALL">
      すべて
    </FilterLink>
    {", "}
    <FilterLink filter="SHOW_ACTIVE">
      アクティブ
    </FilterLink>
    {", "}
    <FilterLink filter="SHOW_COMPLETED">
      完了
    </FilterLink>
  </p>
)

export default Footer

Footer コンポーネントは todos の状態をフィルタリングするためのもので、3 つのフィルタオプションを含んでいます。SHOW_ALL はすべての todos を表示し、SHOW_ACTIVE は追加されたばかりの todo を表示し、SHOW_COMPLETED は完了した todo を表示します。各フィルタオプションは FilterLink コンポーネントに抽出され、フィルタをフィルタリングパラメータとして渡します。

次に container 内の FilterLink のコードです。

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

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

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

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

export default FilterLink

FilterLink コンポーネントもデータのロジックのみを担当し、UI の表示はありません。mapStateToProps では、現在のコンポーネントの props.filter とフィルタ条件 state.visibilityFilter を比較して active 属性を計算し、props に渡します。mapDispatchToProps では onClick メソッドを定義し、現在のフィルタ条件を変更するために action を dispatch します。現在のコンポーネントの filter 属性をパラメータとして渡します。

actions の setVisibilityFilter コードは以下の通りです:

export const setVisibilityFilter = (filter) => ({
  type: 'SET_VISIBILITY_FILTER',
  filter
})

次に Link コンポーネントです。

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

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

  return (
    // eslint-disable-next-line
    <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

Link コンポーネントは props から active、children、onClick 属性値を受け取ります。active が true の場合、Link は span としてレンダリングされ、そうでない場合は a としてレンダリングされます。a タグにはクリックイベントがバインドされ、クリック時に props 内の onClick メソッドが呼び出されます。

これで、インターフェース部分のコードはすべて紹介されました。次に reducer の作成です。
reducer の作成では、reducer の責任をより明確にするために、reducer を todos と visibilityFilter に分割します。todos.js では、todo の追加と状態変更を処理します。コードは以下の通りです:

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

ここでは、todos は空の配列として初期化され、reducer は純粋な関数でなければならず、actions を処理し、state を直接変更することはできず、毎回新しい state を返す必要があります。Todo を追加する際には、配列の展開演算子を使用して state を解構し、新しい todo オブジェクトを配列の最後の値として追加し、最終的に全く新しい state を生成します。todo の状態を変更する際には、配列の map メソッドを使用し、同様に新しい state を返します。map メソッド内では、三項演算子を使用して action パラメータの id と配列の各項目の id を比較し、現在クリックされた todo を見つけ、その completed 値を反転させます。そうでない場合は、現在の todo をそのまま返します。

visibilityFilter.js では、主に todo リストのフィルタ条件を変更するために使用されます。コードは以下の通りです:

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

export default visibilityFilter

この時、state は SHOW_ALL として初期化され、アプリが読み込まれたときにデフォルトで全ての todos が表示されます。actions を処理する際には、action.filter をそのまま返します。

reducer が個別のファイルに分割されると、上記の const で定義された関数名がグローバル状態ツリー内の state 値となります。また、分割された reducer を組み合わせる必要があります。組み合わせる reducer は redux の combineReducers API を使用します。以下は reducers フォルダ内の index.js コードです:

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

const todoApp = combineReducers({
  todos,
  visibilityFilter
})

export default todoApp

最後のステップとして、react-redux の Provider API を使用してアプリ全体をラップします。src 下の index.js では:

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux'
import './index.css';
import App from './App';
import { createStore } from 'redux';
import reducer from './reducers';

const store = createStore(reducer);

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

ブラウザでの実行結果は以下の通りです:

image

これで、全体の To do アプリが開発完了しました。redux を使用した開発と比較して、react-redux は react アプリを開発する際により簡単にしてくれます。

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。