React useStateで作るシンプルToDoアプリ

React HookのuseStateを使ってできるだけシンプルなReactのTodoアプリケーションの作成を行います。
Reactビギナーの人であればuseStateを利用した配列へのオブジェクトの追加、削除、更新方法がわからない人も多いと思います。Todoアプリはそれらの処理をすべて含んでいるのでReactのビギナーな人にとってアプリケーション作成の基礎を学ぶのに一番良い教材です。…XXXのspread operatorsが頻繁に出てくるのでこの記事を通して使いこなせるように慣れていきましょう。

目次
環境の構築
任意のディレクトリでnpx crate-react-appを実行してReactのプロジェクトの開発環境を構築します。
$ npx create-react-app my-app
実行が環境したらmy-appディレクトリに移動してnpm startコマンドを実行します。
$ cd my-app
$ npm start
npm startコマンドが完了したら、ブラウザが自動で起動しReactのトップページが表示されれば環境構築は完了です。
ToDoアプリケーションの作成
作成の準備
Reactインストールディレクトリの下にあるsrcディレクトリの下にあるApp.jsファイルを下記のように変更を行います。TodoのアプリケーションのコードはToDoListファイルの中に記述するためToDoListファイルをimportしています。
import React from 'react';
import TodoList from './components/TodoList';
function App() {
return (
<div style={{ margin: "2em"}}>
<TodoList/>
</div>
);
}
export default App;
TodoList.jsファイルの作成
TodoList.jsファイルはsrcディレクトリの下にcomponentsディレクトリを作成してその下に保存します。
作成開始時のToDoList.jsファイルには下記を記述します。
import React, { useState } from 'react';
function TodoList() {
const initialState = [
{
task: 'Learn vue.js',
isCompleted: false
},
{
task: 'Learn React Hook',
isCompleted: false
},
{
task: 'Learn Gatsby.js',
isCompleted: false
},
]
const [todos, setTodo] = useState(initialState);
return (
<div>
<h1>ToDo List</h1>
</div>
);
}
export default TodoList;
Todoリストの元になる配列initialStateには、3つのタスクがオブジェクトで保存されています。useStateにinitialStateを指定することでtodosにはTodoリストが保存されます。todosの更新にはsetTodoを利用して行います。
ブラウザで確認すると画面にはToDo Listと表示されます。

ToDoリストの表示
ToDoリストを画面上に表示するためにTodoリストが保存されているtodosをmap関数で展開します。
<div>
<h1>ToDo List</h1>
<ul>
{ todos.map((todo, index) => (
<li key={ index }>{ todo.task }</li>
))}
</ul>
</div>
ブラウザで確認を行うとtodosに保存されている3つのタスクがリスト表示されます。

inputタグの設定とuseStateの追加
新しいタスクを追加するためのinputタグをリストの上に追加します。
<h1>ToDo List</h1>
Add Task : <input placeholder="Add New Task" />
<ul>
{ todos.map((todo, index) => (
<li key={ index }>{ todo.task }</li>
))}
</ul>
ブラウザで確認すると入力枠が表示されます。

入力した情報を保持するための新しい変数taskをuseStateで追加します。
const [task, setTask] = useState('')
inputタグに入力した値を取得するためにonChangeイベントを設定し、value属性の値に追加したtaskを指定します。onChangeイベントで実行する関数名は、handleNewTaskとします。
Add Task : <input value={ task } placeholder="Add New Task" onChange={handleNewTask}/>
onChangeイベントを設定すると文字を入力するたびにイベントが発行されます。
イベント時に実行されるhandleNewTask関数を追加します。inputで入力した値はevent.target.valueで取得することができるので、取得できているかconsole.logを利用して確認します。
const handleNewTask = (event) => {
console.log(event.target.value)
}
文字を入力する度にコンソールログに入力した文字が表示されれば正常に動作しているので、console.logからsetTaskに変更を行います。
const handleNewTask = (event) => {
setTask( event.target.value )
}
上記のようにsetTaskに変更すると文字を入力すると入力欄に入力した文字が表示されます。

Formの設定
入力したタスクをtodoリストに保存するためにFormタグを追加する必要があります。FormタグにはonSubmitイベントを追加します。onSubmitイベントで実行する関数名は、handleSubmitとします。onSubmitイベントをEnterを押すとhandelSubmitが実行されます。
<form onSubmit={ handleSubmit }>
Add Task : <input value={ task } placeholder="Add New Task" onChange={handleNewTask}/>
</form>
タスク追加機能の追加
input要素に入力した文字列をtodosに追加するためにhandleSubmit関数を追加します。
const handleSubmit = (event) => {
event.preventDefault()
if(task === '') return
setTodo(todos => [...todos,{ task, isCompleted: false}])
setTask('')
}
event.preventDefault()では通常の動作を停止させています。
handleSubmit関数の中では、useStateで追加した2つの変数のtaskとtodosさらにSetTodoとSetTaskを利用します。
taskには入力欄で入力した文字列が入っているはずなので文字列に何も値がない場合は処理が終わります。文字列が入っている場合は、setTodoによりtodosリストに新しいタスクを追加します。setTaskでは入力欄に入力した文字はTodoリストに追加されるのでその文字を入力欄から削除しています。
下記の部分が既存のTodoリストに入力欄で入力した値を追加している処理です。spread operatorを利用しています。
todos => [...todos,{ task, isCompleted: false}]
ブラウザ上で入力欄に入力した文字がTodoリストに追加されます。追加する際はEnterボタンを押してください。下記では追加により4つのタスクになっています。空白のままEnterを押しても何も起こりません。


setTodoで配列にオブジェクトを追加する前に…todosや{task, isCompleted:false}がどういった形のものなのかコンソールに表示して確認しておきましょう。
if(task === '') return
console.log({task,isCompleted:true});
console.log(...todos);
setTodo(todos => [...todos,{ task, isCompleted: false}])
setTask('')
}
下記のような形であることが確認できます。

タスクの削除機能を追加
タスクの追加が行えたので次はタスクの削除機能を追加します。
liタグにspanタグで囲んでXを追加します。
<li key={ index }>{ todo.task } <span>X</span></li>
タスクの右にXが表示されます。

このXをクリックするとタスクが削除されるように設定を行なっていきます。spanタグにonClickイベントを追加し、関数をhandleRemoveTaskとします。
<li key={ index }>{ todo.task } <span onClick={ () => handleRemoveTask(index) }>X</span></li>
handleRemoveTask関数を追加します。handleRemoveTaskでは削除するタスクを識別するためにindexを渡します。
const handleRemoveTask = index => {
const newTodos = [...todos]
newTodos.splice(index,1)
setTodo(newTodos)
}
handleRemoveTaskの中では現在のTodoリストをspread operatorを利用して新しい配列newTodosに保存します。spliceメソッドを利用して配列のindex番目の要素を1つ削除しています。削除後はsetTodoで新しいTodoリストで既存のTodoリストを書き換えています。
設定後、Xをクリックするとタスクが削除することができます。3つのタスクが削除になり2つになっています。すべてのタスクを削除することも可能です。

タスクの削除ではfilter関数を利用することもできます。filter関数では削除を行うのではなくindexを持ったtodoを取り除いています。
const handleRemoveTask = index => {
const newTodos = [...todos].filter((todo,todoIndex) => todoIndex !== index);
setTodo(newTodos)
}
ここまでの処理でReact HookのuseStateを利用して、タスクを追加、削除するToDoリストが完成しました。完成時の動作は下記の通りです。

タスク更新機能の追加
Xをクリックするとタスク一覧から削除を行っていましたが、削除ではなくタスクの完了か未完了かを表すisCompletedの値を更新します。
handleRemoveTaskをhandleupdateTaskに変更します。
<span onClick={ () => handleUpdateTask(index) }>X</span>
isCompletedがtrueの場合はタスクが完了したのでタスクが完了したことを表すためstyleのtextDecorationLineをline-throughに文字列の上に横棒を設定します。
<ul>
{ todos.map((todo, index) => (
<li
key={ index }
style={ todo.isCompleted === true ? {textDecorationLine: 'line-through'}:{}}
>
{ todo.task }
<span onClick={ () => handleUpdateTask(index) }>X</span></li>
))
}
</ul>
最後にhandleUpdateTaskメソッドを追加します。handleUpdateTaskの引数で設定したindexとtodos配列のindexが一致したtodoのisCompletedの値を現在設定されている値を逆の値に設定しています。trueであればfalseになり、falseであればtrueになります。
const handleUpdateTask = index => {
let newTodos = todos.map((todo,todoIndex) => {
if(todoIndex === index){
todo.isCompleted = !todo.isCompleted
}
return todo;
})
setTodo(newTodos);
}
Xをクリックすると横棒が表示され、横棒が表示されている状態でクリックすると横棒が非表示となります。

作成したコード
作成したコード全体は下記となります。
import React, { useState } from 'react';
function TodoList() {
const initialState = [
{
task: 'Learn vue.js',
isCompleted: false
},
{
task: 'Learn React Hook',
isCompleted: false
},
{
task: 'Learn Gatsby.js',
isCompleted: false
},
]
const [todos, setTodo] = useState(initialState);
const [task, setTask] = useState('')
const handleNewTask = (event) => {
setTask( event.target.value)
}
const handleSubmit = (event) => {
event.preventDefault()
if(task === '') return
setTodo(todos => [...todos,{ task, isCompleted: false}])
setTask('')
}
const handleRemoveTask = index => {
const newTodos = [...todos]
newTodos.splice(index,1)
setTodo(newTodos)
}
return (
<div>
<h1>ToDo List</h1>
<form onSubmit={ handleSubmit }>
Add Task : <input value={ task } placeholder="Add New Task" onChange={handleNewTask}/>
</form>
<ul>
{ todos.map((todo, index) => (
<li key={ index }>{ todo.task } <span onClick={ () => handleRemoveTask(index) }>X</span></li>
))}
</ul>
</div>
);
}
export default TodoList;