前回は、Todoアプリケーションの追加、削除機能を実装しました。
コンテクストの導入
Reactのように状態管理を活用するフレームワークには、prop drilling問題がついてまわります。
共有したデータを子Componentに渡していくことで、開発規模が大きくなるにつれ、状態管理が難しくなっていく問題です。
従来は、reduxを使って、コンポーネント間で状態の管理を行なっていましたが、Reactのコンテクストを利用することで、reduxを使わなくても状態管理がしやすくなりました。
1 2 |
mkdir src/contexts touch src/contexts/AppContext.js |
このファイルにコンテクストオブジェクトを作成する実装を追加します。
1 2 3 4 5 6 7 8 |
// src/contexts/AppContext.js // https://ja.reactjs.org/docs/context.html#reactcreatecontext import { createContext } from 'react' const AppContext = createContext() export default AppContext |
今後は、このコンテクストを子コンポーネントに渡すことで状態管理を行います。
このコンテクストを使うには、Providerを作成する必要があります。
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 |
// 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/' import AppContext from '../contexts/AppContext' const App = () => { // 引数: 作成したreducer(初期状態) const [state, dispatch] = useReducer(reducer, []) return ( <AppContext.Provider value={{state, dispatch}}> <div className="container-fluid"> <TodoForm /> <Todos /> </div> </AppContext.Provider> ) } export default App; |
コンテクストを渡してあげたいコンポーネントを「AppContext.Provider」で囲みます。
続いて、子コンポーネントを以下のように修正します。
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 |
// src/components/Todos.js import React, {useContext} from 'react' import Todo from './Todo' import AppContext from '../contexts/AppContext' // 引数なし const Todos = () => { // State取得 const {state} = useContext(AppContext) return( <> <h4>予定一覧</h4> <table className="table table-hover"> <thead> <tr> <th>ID</th> <th>表題</th> <th>内容</th> <th></th> </tr> </thead> <tbody> {/* dispatch削除 */} { state.map((todo, index) => (<Todo key={index} todo={todo} />))} </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 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, useContext } from 'react' // useContext追加 import AppContext from '../contexts/AppContext' import { CREATE_TODO, DELETE_ALL_TODOS } from '../actions' // 引数削除 const ToDoForm = () => { // state取得 const { state, dispatch } = useContext(AppContext) const [title, setTitle] = useState('') const [body, setBody] = useState('') // Todoの追加 const addTodo = e => { e.preventDefault() dispatch({ type: CREATE_TODO, 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 |
// src/components/Todo.js import React, {useContext} from 'react' // useContext追加 import { DELETE_TODO } from '../actions' import AppContext from '../contexts/AppContext' // 引数からdispatch削除 const Todo = ({todo}) => { // contextからdispatch取得 const { dispatch } = useContext(AppContext) 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 |
これで、stateとdispatchをコンポーネントに渡さなくても、コンテクストを通して値を受け渡しできるようになりました!
Reduxの導入
コンテクストを使うと状態管理は楽になりますが、Reduxを導入してもっと楽をしましょう。
1 2 3 4 5 |
npm info redux redux@4.0.5 | MIT | deps: 2 | versions: 66 npm install redux@4.0.5 |
reduxを使って、reducerを書き換えてみましょう。
コンポーネントの書き換え
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// src/components/App.js const App = () => { /* stateをArray->objectに変更する */ const initialState = { todos: [] } const [state, dispatch] = useReducer(reducer, initialState) ... } |
これまでは、array([])でstateを管理してましたが、オブジェクトに変更しました。
1 2 3 4 |
// src/components/TodoForm.js import React, { useState, useContext } from 'react' // useContext追加 <button type="button" className="btn btn-danger" onClick={deleteAllEvents} disabled={state.todos.length === 0}>全ての予定を削除する</button |
オブジェクトに変更したので、「state.todos.length」に変更しました。
1 2 3 |
// src/components/Todos.js { state.todos.map((todo, index) => (<Todo key={index} todo={todo} />))} |
こちらも「state.map」 -> 「state.todos.map」に変更しました。
reducerの書き換え
1 |
touch src/reducers/todos.js |
このファイルに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 |
// src/reducers/todos.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 |
reduxを使うには、combineReducersを利用すればよさそうです。
1 2 3 4 5 6 |
// reducers/index.js // https://redux.js.org/api/combinereducers import { combineReducers } from 'redux' import todos from './todos' export default combineReducers({ todos }) |
次のコマンドでアプリを動かして、問題なく動作するか確認してみましょう。
1 |
yarn start |

更新履歴のreducer
更新履歴を付けてみましょう。
まずは、reducerを用意します。
1 |
touch src/reducers/historyLogs.js |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
// src/reducers/historyLogs.js import {ADD_HISTORY_LOG, DELETE_HISTORY_LOGS} from '../actions' const historyLogs = (state = [], action) => { switch(action.type) { case ADD_HISTORY_LOG: const historyLog = { description: action.description, historyAt: action.historyAt } return [historyLog, ...state] case DELETE_HISTORY_LOGS: return [] default: return state } } export default historyLogs |
1 2 3 4 5 6 7 |
// reducers/index.js // https://redux.js.org/api/combinereducers import { combineReducers } from 'redux' import todos from './todos' import historyLogs from './historyLogs' export default combineReducers({ todos, historyLogs }) |
1 2 3 4 5 6 7 8 |
// src/actions export const CREATE_TODO = "CREATE_TODO" export const DELETE_TODO = "DELETE_TODO" export const DELETE_ALL_TODOS = "DELETE_ALL_TODOS" // 追加 export const ADD_HISTORY_LOG = 'ADD_HISTORY_LOG' export const DELETE_HISTORY_LOGS = 'DELETE_HISTORY_LOGS' |
1 2 3 4 5 6 7 8 9 10 |
// src/components/App.js const App = () => { /* stateをArray->objectに変更する */ const initialState = { todos: [], historyLogs: [] // 追加 } |
それぞれの意味についは、今までの実装してきた内容とほぼ同じであるため、割愛させていただきます。
時間ユーティリティ
以下のファイルを作成してください。
1 |
touch src/utils.js |
これは、時間を取得するユーティリティです。
1 2 3 4 |
// src/utils.js // [Date.prototype.toISOString](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString) export const timeCurrentIso8601 = () => (new Date()).toISOString() |
履歴時間に使います。
履歴の作成
では、履歴を作成しましょう。
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 |
// src/components/TodoForm.js import React, { useState, useContext } from 'react' // useContext追加 import AppContext from '../contexts/AppContext' import { CREATE_TODO, DELETE_ALL_TODOS, ADD_HISTORY_LOG, DELETE_HISTORY_LOGS } from '../actions' import {timeCurrentIso8601} from '../utils' const ToDoForm = () => { const { state, dispatch } = useContext(AppContext) const [title, setTitle] = useState('') const [body, setBody] = useState('') const addTodo = e => { e.preventDefault() dispatch({ type: CREATE_TODO, title, body }) // 履歴の追加 dispatch({ type: ADD_HISTORY_LOG, description: `「${title}」を追加しました。`, historyAt: timeCurrentIso8601() }) setTitle('') setBody('') } const deleteAllEvents = e => { e.preventDefault() const result = window.confirm('全ての予定を削除してよろしいでしょうか?') if(result) { dispatch({type: DELETE_ALL_TODOS}) // 履歴の削除 dispatch({ type: ADD_HISTORY_LOG, description: '全ての予定を削除しました。', historyAt: timeCurrentIso8601() }) } } ... |
予定追加、削除イベントにdispatchを設定し、履歴更新を行っています。
履歴の削除
履歴の削除も追加しましょう。
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 |
// src/components/TodoForm.js const ToDoForm = () => { ... const deleteHistoryLogs = e => { e.preventDefault() const result = window.confirm("全ての履歴を削除してもよろしいでしょうか?") if (result) { dispatch({ type: DELETE_HISTORY_LOGS }) } } ... } <button type="button" className="btn btn-primary" onClick={addTodo} disabled={createOK}>予定を追加する</button> <button type="button" className="btn btn-danger" onClick={deleteAllEvents} disabled={state.todos.length === 0}>全ての予定を削除する</button> // 追加 <button type="button" className="btn btn-danger" onClick={deleteHistoryLogs} disabled={state.historyLogs.length === 0}>全ての履歴を削除する</button> |
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 |
// src/components/Todo.js import { DELETE_TODO, ADD_HISTORY_LOG // 追加 } from '../actions' import AppContext from '../contexts/AppContext' import { timeCurrentIso8601} from '../utils'; // 追加 // 引数からdispatch削除 const Todo = ({todo}) => { // contextからdispatch取得 const { dispatch } = useContext(AppContext) const {id, title, body} = todo const handleClickDeleteBtn = () => { const result = window.confirm(`予定(id=${id})を完了しますか?`) if (result) { dispatch({ type: DELETE_TODO, id: id}) // 追加 // 履歴の追加 dispatch({ type: ADD_HISTORY_LOG, description: `${title}を完了しました。`, historyAt: timeCurrentIso8601() }) } } return ( ... |
履歴の表示
履歴の表示エリアを作成しましょう。
1 2 |
touch src/components/HistoryLogs.js touch src/components/HistoryLog.js |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// src/components/HistoryLog.js import React from 'react' const HistoryLog = ({historyLog}) => { return ( <tr> <td>{historyLog.description}</td> <td>{historyLog.historyAt}</td> </tr> ) } export default HistoryLog |
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 |
// src/components/HistoryLogs.js import React, {useContext} from 'react' import HistoryLog from './HistoryLog' import AppContext from '../contexts/AppContext' const HistoryLogs = () => { const {state} = useContext(AppContext) return ( <> <br /> <h4>履歴一覧</h4> <table className="table table-hover"> <thead> <tr> <th>内容</th> <th>日時</th> </tr> </thead> <tbody> { state.historyLogs.map((historyLog, index) => { return <HistoryLog key={index} historyLog={historyLog} /> }) } </tbody> </table> </> ) } export default HistoryLogs |
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 |
// 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/' import HistoryLogs from './HistoryLogs' // 追加 import AppContext from '../contexts/AppContext' const App = () => { /* stateをArray->objectに変更する */ const initialState = { todos: [], historyLogs: [] } const [state, dispatch] = useReducer(reducer, initialState) return ( <AppContext.Provider value={{state, dispatch}}> <div className="container-fluid"> <TodoForm /> <Todos /> <HistoryLogs /> // 追加 </div> </AppContext.Provider> ) } export default App; |
この辺りも今まで実装してきた内容とほぼ同じであるため、説明は割愛させていただきます。
では、画面を確認してみましょう。

ローカルストレージへの保存
最後は、定番のローカルストレージへのデータ保存で締めようと思います。
今回は、useEffectも使います。
stateに変化があったときに、コールバックしてくれる便利なメソッドです。
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 |
// src/components/App.js import React, { useEffect, useReducer } from 'react' import 'bootstrap/dist/css/bootstrap.min.css' import TodoForm from './TodoForm' import Todos from './Todos' import reducer from '../reducers/' import HistoryLogs from './HistoryLogs' import AppContext from '../contexts/AppContext' const LOCAL_KEY = 'localKey' const App = () => { // https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse const localState = localStorage.getItem(LOCAL_KEY) const initialState = localState ? JSON.parse(localState) : { todos: [], historyLogs: [] } const [state, dispatch] = useReducer(reducer, initialState) // stateの状態を監視させて、変更があった場合は、第一引数に指定したコールバック関数を実行する useEffect(() =>{ // https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify const string = JSON.stringify(state) localStorage.setItem(LOCAL_KEY, string) // ローカルストレージに値をセット }, [state]) return ( <AppContext.Provider value={{state, dispatch}}> <div className="container-fluid"> <TodoForm /> <Todos /> <HistoryLogs /> </div> </AppContext.Provider> ) } export default App; |
これで画面を更新しても、データが消えなくなりました。
おわりに
いかがだったでしょうか。
もしかしたら「あれ?更新は?」と思われたかもしれません。
更新については、あえて実装していません。
これは皆さんに実装してもらおうと「わざと」残しているのです。
決して、めんどくさくなったとかではありませんから!
是非、更新処理を追加してみてください。
それでは、また!
コメントを残す
コメントを投稿するにはログインしてください。