ただ日々を記すもの

意識高めを装うことができます

ReactとReduxで超簡易のTODOリストを作る

ReactとReduxを使った超簡単なTodoリストを作成していきます。

対象
  • React少し触ったことある人
  • Reduxを使ったことがなくなれたい人

完成形

Image from Gyazo

準備

まずはcreate-react-appでプロジェクトを作成します。
Reactで開発を行うときに大変なのが環境づくり。

webpackいれたり、babelいれたり、、大変なので今回はそのあたりを気にせずReactの開発に集中できるcreate-react-appでプロジェクトを作成します。
インストールは簡単で↓だけ。

create-react-app redux-training

これでReactでの開発が行なえます!

今回はReduxで開発をしていきたいので、作成したredux-trainingに移動し、以下を実行しておきます。

npm install -S redux

TODOアプリを作成

それではアプリを作っていきます。
今回はsrc/index.jsを編集していきます。

viewを作る

まずはTODOを追加するためのテキストボックスとボタンを作っていきましょう。

ボタンなどを表示するTodoAppコンポーネントを作成します。

import React from 'react';
import ReactDOM from 'react-dom';

// 新規に追加
function TodoApp() {
  return (
    <div>
      <input type="text" />
      <input type="button" value="ADD TASK!" />
    </div>
  )
}

// renderするコンポーネントをAppからTodoAppに変更
ReactDOM.render(<TodoApp />, document.getElementById('root'));

これでnpm startを実行すると↓のようになっているかと思います。
f:id:RONKUN:20190202224946p:plain:w350

Reducerの定義

次にReducerを作って行きます。
Reduxを理解する上で重要な登場人物は「Store」「Dispatcher」「Reducer」「Action」になります。

簡単に説明すると、

Store: アプリケーションの状態(State)とロジックを保持
Dispatcher: データの流れを管理。Dispatcherを通じてすべてのActionがStoreに渡される。
Reducer: Storeの状態を変化させるための関数
Action: 単純なObject。dispatchすることでStoreに渡される。

ではReducerを実装していきます。

Reducerは引数でいかの値を受け取ります。

第一引数: State
第二引数: Action(Object)
function tasksReducer(state = initialState, action) {
}

第一引数で渡す状態の初期値を設定して上げる必要があるので、initialStateも合わせて定義します。

const initialState = {
  task: ''
}

そして処理についてですが、
ReducerではActionの内容によって既存の状態を変化させます。

function tasksReducer(state = initialState, action) {
  switch (action.type) {
    case 'INPUT_TASK':
      return {
        ...state,
        task: action.payload.task
      }
    default: return state
  }
}

上記はINPU_TASKというactionが送られてきたときにtaskの値を更新しています。
更新されるのはStoreに保持している値になります。

Storeの定義

Reducerの定義が終わったので次にStoreを定義します。
ReduxはcreateStoreという関数を用意しており、これを実行することでStoreを生成することができます。
Storeを作成する際には、Reducerが必要になります。ここでは、先程定義したtasksReducerを使用してStoreを生成していきます。

import { createStore } from 'redux'

const store = createStore(tasksReducer)

ActionCreatorの定義

ActionCreatorはActionを生成するための関数になります。

const inputTask = (task) => ({
  type: 'INPUT_TASK',
  payload: {
    task: task
  }
})

taskを受け取り、Actionオブジェクトを返すだけの関数です。

さて、これで登場人物は一通り出揃いました。
viewを調整して、入力した値がStoreに渡るようにしてみましょう。

まず、TodoAppコンポーネントにstoreを渡します。

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

そして渡ってきたStoreをTodoAppで使用していきます。

function TodoApp({ store }) {
  // getState()関数はStoreに保持している値を返却する
  // ここではtaskを取り出しています。
  const { task } = store.getState()
  return (
    <div>
      <input type="text" onChange={(e) => store.dispatch(inputTask(e.target.value))} />
      <input type="button" value="ADD TASK!" />
    </div>
  )
}

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

テキストが変更されるごとにstore.dispatchが呼び出されます。
store.dispatch(inputTask(e.target.value))を細かく見ていきましょう。

e.target.value => テキストボックスの入力値を取得
inputTask(e.target.value) => 定義したActionCreatorを呼び出しています。ActionCreatorからはActionオブジェクトが返却されてきます。
store.dispatch(inputTask(e.target.value)) => Actionオブジェクトをstoreにdispatch(送って)しています。

dispatch関数が呼ばれるとStoreに紐づけているReducerが呼ばれ、Actionオブジェクトの内容によってStoreの値を更新してくれます。

ここで一旦ブラウザで挙動を確認してみたいと思います。

少し、storeの更新をする処理を記述する必要があります。

import { render } from 'react-dom'

// 追加
function renderApp(store) {
  render(
    <TodoApp store={store} />,
    document.getElementById('root')
  )
}

store.subscribe(() => renderApp(store))
renderApp(store)
// コメントアウト
// ReactDOM.render(<TodoApp store={store} />, document.getElementById('root'));

新たにrender関数をimportし、今まで使用してReactDom.renderの代わりに使用します。 画面に描画してくれるという面では同じ役割です。

store.suscribe()関数はstoreに更新があるたびに呼び出されます。 ここではstoreの更新のたびにTodoAppコンポーネントがレンダリングされるようにしています。

では、npm startで動作を確認してみます。 動き自体は今までと変わっていませんが、入力値がStoreに渡されています。

Todoをリスト表示

さぁ、次はボタンを押してTodoを追加していきます。

まずtaskを複数保持できるようにtasksという名前の配列を用意します。
加えて、task追加用のActionCreatorも新規に追加します。

const initialState = {
  task: '',
  tasks: [] // 追加!!
}

// 追加
const addTask = (task) => ({
  type: 'ADD_TASK',
  payload: {
    task: task
  }
})

ActionCreatorが用意できたら、このActionオブジェクトがdispatchされたときにStateを更新するためのReducerの処理を追加します。

function tasksReducer(state = initialState, action) {
  switch (action.type) {
    case 'INPUT_TASK':
      return {
        ...state,
        task: action.payload.task
      }
    // 追加
    case 'ADD_TASK':
      return {
        ...state,
        tasks: state.tasks.concat([action.payload.task])
      }
    default: return state
  }
}

ここでポイントなのは、
tasksを追加するときの処理。
tasks: state.tasks.concat([action.payload.task])
ここはpushではなくconcatやspread operatorを使うようにします。

理由は、pushは現在の状態であるstate自体に変更が加わってしまい、予期せぬ副作用を起こすことがあるからです。
そのため新しい配列を生成する、concatやSpread Operator(...を使うやつ)を使用します。

最後に、TodoAppコンポーネントを変更していきます。

function TodoApp({ store }) {
  const { task, tasks } = store.getState() // リスト表示するためにtasksを追加
  return (
    <div>
      <input type="text" onChange={(e) => store.dispatch(inputTask(e.target.value))} />
      {/* 追加 */}
      <input type="button" value="ADD TASK!" onClick={() => store.dispatch(addTask(task))} />
      <ul>
        {tasks.map((task, index) => {
          return (<li key={index}>{task}</li>)
        })}
      </ul>
    </div>
  )
}

これでTodoリストの完成です。
今回はReactとReduxでTodoリストを作成しました。

おまけ1

ここからは理解を深める意味も込めて、簡単な課題になります。

今回のTodoリストだと、タスクを追加したあとテキストボックスに入力値が残ってしまっています。少し気持ち悪いですね。

ここをテキストボックスの入力値がクリアされるようにしてみてください!

おまけ2

今回はすべてindex.jsにドバっと書いていきました。 けどこれってすごく管理しづらいですよね。 この量だからまだいいですが、複雑なアプリケーションを作っていくとなると可読性も下がり、管理しづらくなってしまいます。

そこで、いかのようにディレクトリを分けるようにしてみましょう!

/components
/actions
/reducers

次回のブログでおまけ1,2のコードを載せておきますね。 (おまけの実装はこちら

それでは。

最終的なコード

import React from 'react';
import { render } from 'react-dom';
import { createStore } from 'redux'

const initialState = {
  task: '',
  tasks: []
}

const inputTask = (task) => ({
  type: 'INPUT_TASK',
  payload: {
    task: task
  }
})

const addTask = (task) => ({
  type: 'ADD_TASK',
  payload: {
    task: task
  }
})

function tasksReducer(state = initialState, action) {
  switch (action.type) {
    case 'INPUT_TASK':
      return {
        ...state,
        task: action.payload.task
      }
    case 'ADD_TASK':
      return {
        ...state,
        tasks: state.tasks.concat([action.payload.task])
      }
    default: return state
  }
}

const store = createStore(tasksReducer)

function TodoApp({ store }) {
  const { task, tasks } = store.getState()
  return (
    <div>
      <input type="text" onChange={(e) => store.dispatch(inputTask(e.target.value))} />
      <input type="button" value="ADD TASK!" onClick={() => store.dispatch(addTask(task))} />
      <ul>
        {tasks.map((task, index) => {
          return (<li key={index}>{task}</li>)
        })}
      </ul>
    </div>
  )
}

function renderApp(store) {
  render(
    <TodoApp store={store} />,
    document.getElementById('root')
  )
}

store.subscribe(() => renderApp(store))
renderApp(store)