フロントエンドエンジニアを目指しReactを学んでいます。普段は、Web系のIT企業の開発部門の営業を担当しています。
この記事では、何回かに分けてReactのHooksを使ったTodoリスト作成をまとめていきます。目的は学びを整理するためです。
記事の構成
記事は、以下のような三部作にする予定でしたが、今回は番外編としてフィルター機能の実装にもトライしてみました。
また、前回の課題だった編集機能が空のときに入力できなくするという処理もやってみました。
上記のリンクからそれぞれの記事が見れますのでご笑覧下さい。
記事をおすすめする人
- プログラミング初心者でReactに興味がある人
- ReactのTodoリスト作成で躓いている人
- ReactのHooksについて学んでいる人
記事を読む上での注意点
デザインにはTailwind CSSを使用していますが、ReactやTailwind CSSの設定方法などには触れません。
コードがまだまだ未熟ですので、アドバイスいただけると幸いです。
今回実装する内容
今回行う実装は次の通りです。
- 編集フォームが空のとき入力できないようにする
- タスク未完了 / 完了の選択肢をつくる
- 未完了と完了のタスクをフィルターで分けられるようにする
さっそく実装スタート
編集フォームが空のときは入力できなくする
まず前提からお話すると、前回までは編集の際に空文字でも編集が反映されてしまうということが起きてました。
これでは、ちょっとよくないので「空のときは、もともとのデータを返しなさい」という命令を出すことで「空では入力できない」という処理をしたいと思います。
const submitEdits = (id) => {
const updatedTodoLists = [...todoLists].map((todoList) => {
if (editingTitle === "" || editingContents === "") { // ここと
return alert("文字を入力してください"), todoList; //ここを追加
}
if (todoList.id === id) {
todoList.title = editingTitle;
todoList.contents = editingContents;
todoList.time = new Date().toLocaleString();
}
return todoList;
});
setTodoLists(updatedTodoLists);
setTodoEditing(null);
setEditingTitle("");
setEditingCotents("");
};
if (editingTitle === "" || editingContents === "") return alert("文字を入力してください"), todoList;
上記のようすることで「もし、editingTitleかeditingContentsが空だったら、まず『文字を入力してください』というアラートを出して、そのままもとのtodoListを返してね」という命令を出します。
すると。。。
タイトルか編集内容のどちらかが空だとアラートが出て、Todoはそのままの形で残り、両方しっかり記入することで編集が反映されるようになりました!(ちなみに作成日時も変わってますね。)
タスク完了/未完了でフィルタリングする
次に、以下の処理を実装したいと思います。
・タスク未完了 / 完了の選択肢をつくる
・未完了と完了のタスクをフィルターで分けられるようにする
const App = () => {
const [todoLists, setTodoLists] = useState([]);
const [todoEditing, setTodoEditing] = useState(null);
const [editingTitle, setEditingTitle] = useState("");
const [editingContents, setEditingCotents] = useState("");
const [id, setId] = useState(1);
const [title, setTitle] = useState("");
const [contents, setContents] = useState("");
const [filter, setFilter] = useState("incomplete"); // ここを追加
まずは、useStateを用いて[filter, setFilter]というstateを作ります。初期値はincompleteとします。
<select
value={filter}
onChange={(e) => setFilter(e.target.value)}
className="border px-8 py-3 mb-10"
>
{/* <option value="all">全てのタスク</option> */}
<option value="incomplete">未完了のタスク</option>
<option value="complete">完了したタスク</option>
</select>
選択肢を作るにはタグとタグを使います。
タグの中には、value = { filter } を設定し、イベントハンドラのonChangeには{(e) => setFilter(e.target.value)}という関数を定義します。
加えて、 タグは、未完了のタスクと完了したタスクの2つをつくり、それぞれにincomplete(未完了)、complete(完了)という値を定義します。
初期値をincompleteとすることで、タスクを登録したときに必ず未完了の状態で登録されることになります。
すると、写真のような選択ボタンができました。
これで、何が出来るようになったかというと、未完了のタスク(value = “incomplete” )と完了したタスク(value = “complete”)を切り替えるたびに、useStateで作った [ filter ] の状態を切り替えることが出来るようになります。
- useStateで [ filter, setFilter ]というstateをつくる
- 値の変更ができるsetFilterにonChangeメソッドを使い「値の変更があったらイベントを発火させろ」と命令。setFilter内で( e.target.value )を使うことでタグ内にあるvalue ( incomplete / complete )が変わったときに合わせて状態を変えろと命令する。
- ②の命令によって、ページ上で選択ボタンを切り替える度に、裏側ではfilterの値も切り替わっている。
ここまでの流れをまとめると、こんなイメージでしょうか。
ここからは、Todoリストの中に完了、未完了の選択ボタンを作っていきます。
***省略***
<ul className="flex justify-between mx-20 my-2 ">
<li key={todoList.id}>{todoList.id}</li>
// ここから追加
<li>
<select
value={todoList.status}
onChange={(e) => handleStatusChange(todoList, e)}
>
<option value="incomplete">未完了</option>
<option value="complete">完了</option>
</select>
</li>
// ここまで追加
<li>{todoList.title}</li>
<li>{todoList.contents}</li>
***省略***
次に、未完了のタスクと完了したタスクを分けて表示できるようにします。
const App = () => {
const [todoLists, setTodoLists] = useState([]);
const [todoEditing, setTodoEditing] = useState(null);
const [editingTitle, setEditingTitle] = useState("");
const [editingContents, setEditingCotents] = useState("");
const [id, setId] = useState(1);
const [title, setTitle] = useState("");
const [contents, setContents] = useState("");
const [filteredTodos, setFilteredTodos] = useState([]); // ここを追加
const [filter, setFilter] = useState("incomplete");
まずは、useStateを使って、[filteredTodos, setFilteredTodos] というStateをつくります。
useEffect(() => {
const filteringTodos = () => {
switch (filter) {
case "incomplete": // 受け取った値がincompleteのときは
setFilteredTodos(
todoLists.filter((todoList) => todoList.status === "incomplete")
// filterメソッドを使ってtodoListのstatus(状態)が”incomplete”のもののみを表示してね。
);
break;
case "complete": // 受け取った値がcompleteのときは
setFilteredTodos(
todoLists.filter((todoList) => todoList.status === "complete")
// filterメソッドを使ってtodoListのstatus(状態)が”complete”のもののみを表示してね。
);
break;
default:
setFilteredTodos(todoLists);
// デフォルトの状態ではsetFilterTodosの関数内の(todoLists)を表示してね。
}
};
filteringTodos(); // いずれかの処理の後にfilteringTodos()を実行してね。
}, [filter, todoLists]);
// ただし、実行していいのは、filterかtodoListsに変更があったときだけだよ
//ここが空 [ ] だと、レンダリングされる度に実行してしまう。
次に、useEffectとswitchを使って条件分岐の関数を作ります。
簡単に言うと、「渡ってきたタスクの値が未完了(incomplete)のときは、〇〇の処理をしてね。完了(complete)のときは▲▲の処理をしてね」という命令を書いてあげます。
switchの引数には、先程作ったタグから渡ってきたvalue={filter}が入ります。incompleteかcompleteかで状態を変えるあれです。
関数の最後に[filter, todoLists]と入っていますが、実行の条件を設けることが出来る部分です。fitlerかtodoListが入っているので、この2つのどちらかの状態に変更があったときのみ実行するという指示が出せます。
配列が空 [ ] だと、レンダリングされる度に実行されます。
{filteredTodos.map((todoList) => {
// ① todoLists から filterdTodosに変更
return (
<>
{todoEditing === todoList.id ? (
<>
<input
type="text"
placeholder="タイトルの編集"
className="m-4 p-3 w-4/5 border-2"
value={editingTitle}
onChange={(e) => setEditingTitle(e.target.value)}
/>
<textarea
type="text"
placeholder="内容の編集"
className="mb-7 p-3 w-4/5 border-2"
value={editingContents}
onChange={(e) => setEditingCotents(e.target.value)}
/>
</>
) : (
<div>{""}</div>
)}
// ② ここから追加
<ul className="flex justify-between mx-20 my-2 ">
<li key={todoList.id}>{todoList.id}</li>
<li>
<select
value={todoList.status}
onChange={(e) => handleStatusChange(todoList, e)}
>
<option value="incomplete">未完了</option>
<option value="complete">完了</option>
</select>
</li>
// ここまで追加
<li>{todoList.title}</li>
いよいよ表示部分を作っていきます。
まずは、①のようにmapで展開する状態元をtodoListsからfilteredTodosに切り替えます。
filteredTodosないではm,todoListsがfiliterされていますからこちらに変更することでswitchによる条件分岐を実行した後のtodoListsが表示できるためです。
そして、IDの隣に未完了、完了のボタンを作ります。
こちらのタグの中ではvalueにtodoList.statsを設定し、イベントハンドラのonChangeメソットで、完了と未完了を切り替える度に todoListとeventを渡すhandleStateChangeという関数が実行されるようにします。
const handleStatusChange = ({ id }, e) => {
① const newTodos = todoLists.map((todoList) => ({ ...todoList }));
② setTodoLists(
newTodos.map((todoList) =>
todoList.id === id ? { ...todoList, status: e.target.value } : todoList
)
);
};
そして最後に、hangleStateChange関数内で実行したい処理を書いていきます。
実行する処理は、以下の流れです。
- todoListをmapメソッドを使って繰り返し処理、そしてさらにtodoListをスプレット構文を使って展開するという命令を出す。そして、その処理をnewTodosという変数に代入。
- setTodoLists関数内でnewTodosをmapメソッドをつかって繰り返し処理(このとき新しい配列になっている)、そして三項演算子を用いて、引数で渡ってきたidとtodoListのid が同じなら、todoListを分割して、statusにはe.target.value (今回はincompleteかcomplete)を受け取る。idが異なればもともとのtodoListを返せと命令を出す。
このようにすると。。。
タスクの未完了のステータスを完了に変えると、未完了のTodoリストから消えて、完了したタスクの方に移りました。
これでフィルタリングもできました。
これまでのコード
import React, { useState, useEffect } from "react";
const App = () => {
const [todoLists, setTodoLists] = useState([]);
const [todoEditing, setTodoEditing] = useState(null);
const [editingTitle, setEditingTitle] = useState("");
const [editingContents, setEditingCotents] = useState("");
const [id, setId] = useState(1);
const [title, setTitle] = useState("");
const [contents, setContents] = useState("");
const [filteredTodos, setFilteredTodos] = useState([]);
const [filter, setFilter] = useState("incomplete");
useEffect(() => {
const temp = localStorage.getItem("keepTodo");
const loadedTodo = JSON.parse(temp);
if (loadedTodo) {
setTodoLists(loadedTodo);
}
}, []);
useEffect(() => {
const temp = JSON.stringify(todoLists);
localStorage.setItem("keepTodo", temp);
}, [todoLists]);
const addTodo = (e) => {
e.preventDefault();
handleOnSubmit();
};
useEffect(() => {
const filteringTodos = () => {
switch (filter) {
case "incomplete":
setFilteredTodos(
todoLists.filter((todoList) => todoList.status === "incomplete")
);
break;
case "complete":
setFilteredTodos(
todoLists.filter((todoList) => todoList.status === "complete")
);
break;
default:
setFilteredTodos(todoLists);
}
};
filteringTodos();
}, [filter, todoLists]);
const handleOnSubmit = () => {
const newTodo = {
id: Math.floor(Math.random() * 1000),
title: title,
contents: contents,
status: "incomplete",
time: new Date().toLocaleString(),
};
if (title === "" || contents === "") {
return alert("文字を入力してください");
} else {
setId(id + 1);
setTodoLists([...todoLists, newTodo]);
setTitle("");
setContents("");
}
};
const handleAllDelete = () => {
const confirm = window.confirm("本当にすべてを削除しますか");
if (confirm) return setTodoLists([]);
};
const deleteListButton = (id) => {
const newArray = todoLists.filter((todoLists) => todoLists.id !== id);
setTodoLists(newArray);
};
const submitEdits = (id) => {
const updatedTodoLists = [...todoLists].map((todoList) => {
if (editingTitle === "" || editingContents === "") {
return alert("文字を入力してください"), todoList;
}
if (todoList.id === id) {
todoList.title = editingTitle;
todoList.contents = editingContents;
todoList.time = new Date().toLocaleString();
}
return todoList;
});
setTodoLists(updatedTodoLists);
setTodoEditing(null);
setEditingTitle("");
setEditingCotents("");
};
const handleStatusChange = ({ id }, e) => {
const newTodos = todoLists.map((todoList) => ({ ...todoList }));
setTodoLists(
newTodos.map((todoList) =>
todoList.id === id ? { ...todoList, status: e.target.value } : todoList
)
);
};
return (
<>
<h1 className="text-center text-3xl font-bold mt-10">
Todo List Practice
</h1>
<div className="text-center mt-7">
<form onSubmit={addTodo}>
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="タイトルを入力"
className=" p-3 w-4/5 border-2"
/>
<textarea
type="text"
value={contents}
onChange={(e) => setContents(e.target.value)}
placeholder="内容を入力"
className="mt-5 p-3 w-4/5 border-2"
/>
<div className="flex justify-center my-5">
<button
type="submit"
className="bg-indigo-700 text-white rounded mx-5 px-10"
>
追加
</button>
<button
onClick={handleAllDelete}
disabled={todoLists.length === 0}
className="bg-red-700 text-white rounded mx-5 px-5 py-2"
>
すべて削除
</button>
</div>
</form>
<h3 className="text-xl font-bold text-center my-10">Todo一覧</h3>
<select
value={filter}
onChange={(e) => setFilter(e.target.value)}
className="border px-8 py-3 mb-10"
>
<option value="incomplete">未完了のタスク</option>
<option value="complete">完了したタスク</option>
</select>
{/* 以降map開始 */}
<div className="mt-3 container mx-auto">
{filteredTodos.map((todoList) => {
return (
<>
{todoEditing === todoList.id ? (
<>
<input
type="text"
placeholder="タイトルの編集"
className="m-4 p-3 w-4/5 border-2"
value={editingTitle}
onChange={(e) => setEditingTitle(e.target.value)}
/>
<textarea
type="text"
placeholder="内容の編集"
className="mb-7 p-3 w-4/5 border-2"
value={editingContents}
onChange={(e) => setEditingCotents(e.target.value)}
/>
</>
) : (
<div>{""}</div>
)}
<ul className="flex justify-between mx-20 my-2 ">
<li key={todoList.id}>{todoList.id}</li>
<li>
<select
value={todoList.status}
onChange={(e) => handleStatusChange(todoList, e)}
>
<option value="incomplete">未完了</option>
<option value="complete">完了</option>
</select>
</li>
<li>{todoList.title}</li>
<li>{todoList.contents}</li>
<li>
<button onClick={() => deleteListButton(todoList.id)}>
削除
</button>
{todoList.id === todoEditing ? (
<button
onClick={() => submitEdits(todoList.id)}
disabled={false}
className="ml-7"
>
編集を保存
</button>
) : (
<button
onClick={() => setTodoEditing(todoList.id)}
className="ml-7"
>
編集
</button>
)}
</li>
</ul>
<div className="flex justify-center ">
<div className="text-xs m-5">作成日時:{todoList.time}</div>
</div>
<div className="mx-auto border-b-2 border-gray-200 w-5/6 my-4"></div>
</>
);
})}
</div>
</div>
</>
);
};
export default App;
まとめ
第4回目の記事では番外編として、編集機能を改善し、空の入力では変更が反映されないようにしたのと、フィルターの実装を行いました。
まだまだ改善すべき点はたくさんあると思います。
例えば、編集の際は入力欄に初期値としてもともと入力してた文字が入るべきだと思うし(試行錯誤したができなかった)、実はIDも連番にしたかったのにリロードすると番号が1に戻ってしまうという自体に直面し、解決できずに、ランダムIDに戻しました。。。
ただ、ReactのHooksやJavascriptのmap,filter, switchや三項演算子による条件分岐などなど、Reactの基礎の基礎となる部分は以前よりは身についたのかなと思います。
まだ自信がないですが、コツコツがんばります。