ReactとReduxを使った超簡単なTodoリストを作成していきます。
対象
- React少し触ったことある人
- Reduxを使ったことがなくなれたい人
完成形
準備
まずは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
を実行すると↓のようになっているかと思います。
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)