Todoアプリは、プログラミング初学者の方が学習するには、うってつけです。
何故なら、Todoアプリには、登録・参照・更新・削除のいわゆるCRUD(クラッド)と呼ばれる機能を詰め込み、かつ、シンプルに実装できるからです。
以前、Javascriptで実装したこともありますが、今回は、Reactを使ってコーディングしていこうと思います。
事前準備
下記を参考に「create-react-app」を使えるようにしてください。
また、任意の場所に以下のプロジェクトを作成します。
1 2 3 4 5 |
mkdir react-todo cd react-todo create-react-app todos cd todos |
次のコマンドで、正常にプロジェクトが起動するか確認します。
1 |
yarn start |

プロジェクトの構成
デフォルトのプロジェクト構成を少し変更します。
1 2 3 4 5 6 7 8 |
mkdir src/components mv src/App.js src/components/ mv src/App.css src/components/ rm src/serviceWorker.js rm src/setupTests.js rm src/App.test.js rm src/logo.svg rm .gitignore |
src配下は以下のようになりました。
1 2 3 4 5 6 7 8 |
src | L components | L App.css | L App.js |-- index.js |-- index.css |-- package.json |-- yarn.lock |
src/index.jsの中身を修正します。
1 2 3 4 5 6 7 8 9 10 11 12 |
// src/index.js import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './components/App'; ReactDOM.render( <React.StrictMode> <App /> </React.StrictMode>, document.getElementById('root') ); |
これでOKです。
Bootstrapの導入
先に、Bootstrapを導入してしまいましょう。
1 2 3 4 5 6 7 |
# バージョン確認 npm info bootstrap bootstrap@4.4.1 | MIT | deps: none | versions: 31 # インストール yarn add bootstrap@4.4.1 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
// src/components/App.js import React from 'react' // https://getbootstrap.com/docs/4.4/getting-started/webpack/#importing-compiled-css import 'bootstrap/dist/css/bootstrap.min.css'; const App = () => { return ( <div className="container-fluid"> <h4>私の予定</h4> <form> <div className="form-group"> <label htmlFor="formTodoTitle">表題</label> <input className="form-control" id="formTodoTitle" /> </div> <div className="form-group"> <label htmlFor="formTodoBody">内容</label> <textarea className="form-control" id="formTodoBody" /> </div> <button className="btn btn-primary">予定を追加</button> <button className="btn btn-danger">全ての予定を削除する</button> </form> <br /> <h4>予定一覧</h4> <table className="table table-hover"> <thead> <tr> <th>ID</th> <th>表題</th> <th>内容</th> </tr> </thead> <tbody> </tbody> </table> </div> ) } export default App; |
画面を表示してみましょう。

Reducerの導入
Reactには、Stateと呼ばれるアプリケーションの状態を保持する機能があります。
アプリケーションは、ユーザーからのアクションにより、常に状態が変化します。
Todoリストで例を挙げてみましょう。
例えば、予定を追加した時に、リストのアイテム数が1つ増えます。
逆に削除した場合は、リストのアイテムが1つ減ります。
このリストのアイテムは、特定のアクションにより、アイテム個数の状態が変化します。
Reactでは、このリストアイテムの要素をStateで管理するわけです。
そして、その状態管理を担う機能の一つが、Reducerとなります。
これは、Reduxとよばれるフレームワークの機能ですが(後で導入します)、useReducerを使うとReactでも使用可能です。
以下のフォルダとファイルを作成してください。
1 2 |
mkdir src/reducers touch src/reducers/index.js |
このファイルには、状態管理によって処理したいことを書きます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// reducers/index.js // state -> 状態管理 // action -> 状態が変更されるきっかけとなったアクション const todos = (state=[], action) =>{ switch(action.type) { // Todo Itemを作成 case 'CREATE_TODO': const todo = { title: action.title, body: action.body } const length = state.length const id = length === 0 ? 1 : state[length - 1].id + 1 // ...stateで展開、 idは、keyと同じなのでkey:keyを省略、...eventで残りのkeyを展開 return [...state, {id, ...todo}] case 'DELETE_TODO': // 後で実装 return state case 'DELETE_ALL_TODOS': // 全てのToDo Itemを削除 return [] default: return state } } export default todos |
Reducerは、アプリケーションの状態を変えるために利用します。
要素としては、「state」と「action」の二つが存在します。
以下のように利用します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
// src/components/App.js import React, { useState, useReducer } from 'react' // https://getbootstrap.com/docs/4.4/getting-started/webpack/#importing-compiled-css import 'bootstrap/dist/css/bootstrap.min.css' import reducer from '../reducers/index' import Todo from './Todo' const App = () => { // 引数: 作成したreducer(初期状態) const [state, dispatch] = useReducer(reducer, []) // useStateは、stateから値を取得したり、セットしたりできる const [title, setTitle] = useState('') const [body, setBody] = useState('') // Todoの追加 const addTodo = e => { // 画面リロードを防ぐ e.preventDefault() // dispatchは、状態遷移させたいタイミングで引数にactionを指定して発火 dispatch({ type: 'CREATE_TODO', // reducers/index.jsにSwitchの文字を指定 title, body }) // 初期値に戻す setTitle('') setBody('') } return ( <div className="container-fluid"> <h4>私の予定</h4> <form> <div className="form-group"> <label htmlFor="formTodoTitle">表題</label> <input className="form-control" id="formTodoTitle" value={title} onChange={e => setTitle(e.target.value)} /> </div> <div className="form-group"> <label htmlFor="formTodoBody">内容</label> <textarea className="form-control" id="formTodoBody" value={body} onChange={e => setBody(e.target.value)} /> </div> <button type="button" className="btn btn-primary" onClick={addTodo}>予定を追加する</button> <button type="button" className="btn btn-danger">全ての予定を削除する</button> </form> <br /> <h4>予定一覧</h4> <table className="table table-hover"> <thead> <tr> <th>ID</th> <th>表題</th> <th>内容</th> <th></th> </tr> </thead> <tbody> { state.map((todo, index) => (<Todo key={index} todo={todo} dispatch={dispatch} />))} </tbody> </table> </div> ) } export default App; |
上記の実装では、「import Todo from ‘./Todo’」の部分でエラーになると思います。Evnet.jsファイルを作成していないからです。
ひとまず、ここは無視してください。
次のインポートでは、useStateとuseReducerを取り込んでいます。
1 |
import React, { useState, useReducer } from 'react' |
続いて、以下の文でReducerの初期値を設定しています。
1 |
const [state, dispatch] = useReducer(reducer, []) |
また、次の処理でstateの状態を取得、設定しています。
1 2 |
const [title, setTitle] = useState('') const [body, setBody] = useState('') |
最後に、目的のreducerを発動させるためにdispatch関数の利用の例を以下に示します。
1 2 3 4 5 |
dispatch({ type: 'CREATE_TODO', // reducers/index.jsにSwitchの文字を指定 title, body }) |
ここでは、作成ボタンを押下したらdispatchが動作するようになっています。
Todoコンポーネントの作成
続いて、Todoコンポーネントを作成しましょう。
1 |
touch src/components/Todo.js |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// src/components/Todo.js import React from 'react' const Todo = ({dispatch, todo}) => { const {id, title, body} = todo const handleClickDeleteBtn = () => { const result = window.confirm(`予定(id=${id})を本当に削除しても良いですか?`) if (result) dispatch({ type: 'DELETE_TODO', id: id}) } return ( <tr> <td>{id}</td> <td>{title}</td> <td>{body}</td> <td><button type="button" className="btn btn-danger" onClick={handleClickDeleteBtn}>完了</button></td> </tr> ) } export default Todo |
「予定を追加する」ボタンを押下したら予定一覧にアイテムが表示されるようになりました。

ついでに、実装途中で合ったアイテムの削除を実装しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// reducers/index.js // state -> 状態管理 // action -> 状態が変更されるきっかけとなったアクション const todos = (state=[], action) =>{ switch(action.type) { // Todo Itemを作成 case 'CREATE_TODO': const todo = { title: action.title, body: action.body } const length = state.length const id = length === 0 ? 1 : state[length - 1].id + 1 // ...stateで展開、 idは、keyと同じなのでkey:keyを省略、...eventで残りのkeyを展開 return [...state, {id, ...todo}] case 'DELETE_TODO': // 追加 return state.filter(todo => todo.id !== action.id) # 修正 case 'DELETE_ALL_TODOS': // 全てのToDo Itemを削除 return [] default: return state } } export default todos |
これで完了ボタンを押下したら、アイテムを削除できるようになっているはずです。



全ての予定を削除する。
全ての予定を削除する機能を実装しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// src/components/App.js // Todoの削除 const deleteAllEvents = e => { e.preventDefault() const result = window.confirm('全ての予定を削除してよろしいでしょうか?') if(result) dispatch({type: 'DELETE_ALL_TODOS'}) } // 予定追加ボタンの活性制御 const createOK = title === '' || body === '' <button type="button" className="btn btn-primary" onClick={addTodo} disabled={createOK}>予定を追加する</button> <button type="button" className="btn btn-danger" onClick={deleteAllEvents} disabled={state.length === 0}>全ての予定を削除する</button> |
「全ての予定を削除する」を押下するとアイテムが全件削除できるようにしました。
リファクタリング
App.jsの記述が大きくなってきたので、リファクタリングしましょう。
まずは、以下のファイルを作成します。
1 2 |
touch src/components/Todos.js touch src/components/TodoForm.js |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
// src/components/TodoForm.js import React, { useState } from 'react' const ToDoForm = ({state, dispatch}) => { // useStateは、stateから値を取得したり、セットしたりできる const [title, setTitle] = useState('') const [body, setBody] = useState('') // Todoの追加 const addTodo = e => { // 画面リロードを防ぐ e.preventDefault() // dispatchは、状態遷移させたいタイミングで引数にactionを指定して発火 dispatch({ type: 'CREATE_TODO', // reducers/index.jsにSwitchの文字を指定 title, body }) // 初期値に戻す setTitle('') setBody('') } // Todoの削除 const deleteAllEvents = e => { e.preventDefault() const result = window.confirm('全ての予定を削除してよろしいでしょうか?') if(result) dispatch({type: 'DELETE_ALL_TODOS'}) } // 予定追加ボタンの活性制御 const createOK = title === '' || body === '' return ( <React.Fragment> <h4>私の予定</h4> <form> <div className="form-group"> <label htmlFor="formTodoTitle">表題</label> <input className="form-control" id="formTodoTitle" value={title} onChange={e => setTitle(e.target.value)} /> </div> <div className="form-group"> <label htmlFor="formTodoBody">内容</label> <textarea className="form-control" id="formTodoBody" value={body} onChange={e => setBody(e.target.value)} /> </div> <button type="button" className="btn btn-primary" onClick={addTodo} disabled={createOK}>予定を追加する</button> <button type="button" className="btn btn-danger" onClick={deleteAllEvents} disabled={state.length === 0}>全ての予定を削除する</button> </form> <br /> </React.Fragment> ) } export default ToDoForm |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
// src/components/Todos.js import React from 'react' import Todo from './Todo' const Todos = ({state, dispatch}) => { return( <> <h4>予定一覧</h4> <table className="table table-hover"> <thead> <tr> <th>ID</th> <th>表題</th> <th>内容</th> <th></th> </tr> </thead> <tbody> { state.map((todo, index) => (<Todo key={index} todo={todo} dispatch={dispatch} />))} </tbody> </table> </> ) } export default Todos |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// src/components/App.js import React, { useReducer } from 'react' import 'bootstrap/dist/css/bootstrap.min.css' import TodoForm from './TodoForm' import Todos from './Todos' import reducer from '../reducers' const App = () => { // 引数: 作成したreducer(初期状態) const [state, dispatch] = useReducer(reducer, []) return ( <div className="container-fluid"> <TodoForm state={state} dispatch={dispatch} /> <Todos state={state} dispatch={dispatch} /> </div> ) } export default App; |
App.jsがだいぶすっきりしましたね。
こんどは、reducerをリファクタリングしましょう。
1 2 |
mkdir src/actions touch src/actions/index.js |
1 2 3 4 |
// src/actions export const CREATE_TODO = "CREATE_TODO" export const DELETE_TODO = "DELETE_TODO" export const DELETE_ALL_TODOS = "DELETE_ALL_TODOS" |
べた書きしていたactionを別ファイルに切り出す作戦です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// reducers/index.js import { CREATE_TODO, DELETE_ALL_TODOS, DELETE_TODO} from '../actions' const todos = (state=[], action) =>{ switch(action.type) { // Todo Itemを作成 case CREATE_TODO: const todo = { title: action.title, body: action.body } const length = state.length const id = length === 0 ? 1 : state[length - 1].id + 1 // ...stateで展開、 idは、keyと同じなのでkey:keyを省略、...eventで残りのkeyを展開 return [...state, {id, ...todo}] case DELETE_TODO: // 後で実装 return state.filter(todo => todo.id !== action.id) case DELETE_ALL_TODOS: // 全てのToDo Itemを削除 return [] default: return state } } export default todos |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// src/components/TodoForm.js import { CREATE_TODO, DELETE_ALL_TODOS } from '../actions' ... dispatch({ type: CREATE_TODO, // reducers/index.jsにSwitchの文字を指定 title, body }) if(result) dispatch({type: DELETE_ALL_TODOS}) |
1 2 3 4 5 |
// src/components/Todo.js import { DELETE_TODO } from '../actions' if (result) dispatch({ type: DELETE_TODO, id: id}) |
お疲れ様でした。今回は、ここまでにしましょう。
次回
Reduxを導入して、Todoアプリをパワーアップしましょう。
コメントを残す
コメントを投稿するにはログインしてください。