ReactのフレームワークのNext.jsを使って定番のタスクアプリを作っていきます。
作るもの
こんな感じの簡単なタスクアプリを作ってみます。
- テキストに入力した値を「追加」ボタンでタスクとして追加できます。
- 「削除」ボタンでタスクを削除できます。
- 「すべてのタスク」、「ゴミ箱」のフィルターでタスクを削除済みかそうでないかに分けています。
- 「ゴミ箱を空にする」ボタンで削除したタスクを完全に消去できます。
こちらの記事を参考にさせていただきました。かなり詳しく書いてありました。
環境構築
Next.jsの環境構築をします。
npx create-next-app --ts --use-npm your-project
create-next-appでNext.jsのプロジェクトの作成です。
--ts でTypeScriptを使用します。
--use--npm でnpmを優先して使用します。
your-projectはプロジェクトの名前です。
プロジェクトファイルに移動します。
cd your-project
サーバーを立ちあげてみます。
npm run dev
ターミナル上のurl: http://localhost:3000をcommandを押しながらクリックすることで以下のページに飛ぶことができます。
Tailwind CSS の導入
CSSはTailWindCSSが使いやすいと思います。以下のサイトを参考に導入しました。
基本的な入力フォームの作成
タスクアプリを実装していきます。
まず、pages/index.tsxのデフォルトの内容を全て削除して以下の内容に書き換えます。
const App = () => {
return (
<div className="w-full mx-auto max-w-2xl my-6 px-3">
<form onSubmit={(e) => e.preventDefault()}>
{/* テキスト入力フォーム */}
<input
className="border border-black"
type="text"
value=""
onChange={(e) => e.preventDefault()}
/>
{/* 追加ボタン */}
<input
type="submit"
value="追加"
onSubmit={(e) => e.preventDefault()}
/>
</form>
</div>
);
};
export default App;
以下のように入力フォームと追加ボタンが表示されます。
e.preventDefault()によりonSubmitやonChangeのデフォルトの挙動がキャンセルされています。
フォームに文字を入力できるようにする
フォームに入力された文字列をステイトとして保持するため、useStateを導入します。
//フォームに入力された値をtodoに登録するまで保持しておくためのstate
const [text, setText] = useState('');
-
useState: - 引数となるのはステイトの初期値
- 現在のステイト
textとそれを更新するための関数setTextを返す。 -
text: 現在のステイトの値 -
setText: ステイトを更新するメソッド-
'set' + ステイト(ロワーキャメルケース)で書きます。
-
-
useStateの引数:ステイトの初期値(=空の文字列)
// React から useState をインポート
+ import { useState } from 'react';
const App = () => {
+ const [text, setText] = useState('');
return (
<div className="w-full mx-auto max-w-2xl my-6 px-3">
<form onSubmit={(e) => e.preventDefault()}>
{/*
入力中テキストの値を text ステイトが
持っているのでそれを value として表示
onChange イベント(=入力テキストの変化)を
text ステイトに反映する
*/}
<input
className="border border-black"
type="text"
- value=""
+ value={text}
onChange={(e) => setText(e.target.value)} />
<input
type="submit"
value="追加"
onSubmit={(e) => e.preventDefault()}
/>
</form>
</div>
);
};
export default App;
todoをリストとして追加できるようにする
Todoオブジェクトの型の定義
1つのtodoをオブジェクトとすると、そのオブジェクトにはタスクの内容を保持するプロパティが必要となり、valueプロパティとして持ちます。
入力フォームに入力されたテキスト文字列が代入されるため、value プロパティはstring型となります。
これから作成される複数のtodoの雛形としてTodo 型オブジェクトの型エイリアスを定義します。
+ type Todo = {
+ value: string;
+ };
const App = () => {
ステイトとして保持するタスクたち(todos)はTodo 型オブジェクトの配列となります。
const App = () => {
const [text, setText] = useState('');
+ const [todos, setTodos] = useState<Todo[]>([]);
return (
todosステイトを更新する
todosステイトを更新(=新しいタスクの追加)していきます。
ステイトを更新するコールバック関数を作成します。
const [todos, setTodos] = useState<Todo[]>([]);
// todos ステイトを更新する関数
+ const handleOnSubmit = () => {
+ // formの内容が空白の場合はalertを出す
+ if (text === "") {
+ alert("文字を入力してください");
+ return;
+ }
// 新しい Todo を作成
+ const newTodo: Todo = {
+ value: text,
+ };
+ // スプレッド構文を用いて todos ステイトのコピーへ newTodo を追加する
+ setTodos([newTodo, ...todos]);
+ // フォームへの入力をクリアする
+ setText('');
+ };
onSubmitイベントに紐付ける
コールバックとして() => handleOnSubmit()もしくはhandleOnSubmit関数そのものを渡します。handleOnSubmit()のみだと即時に実行されるためです。
return (
<div className="w-full mx-auto max-w-2xl my-6 px-3">
{/*コールバックとして () => handleOnSubmit()を渡す */}
<form
onSubmit={(e) => {
e.preventDefault();
+ handleOnSubmit();
+ }}
>
<input
className="border border-black"
type="text"
value={text}
onChange={(e) => setText(e.target.value)} />
<input
type="submit"
value="追加"
+ onSubmit={handleOnsubmit}
/>
</form>
</div>
);
onSubmitイベントが発火するとhandleOnSubmit関数が実行され、todosステイトを更新(=新しいタスクを追加)します。
todosステイトを展開してページに表示する
todosステイトを展開して、タスク一覧としてページに表示します。
todos (=配列)を非破壊メソッドのArray.prototype.map()を使って<li></li>タグへ展開します。
Reactではリストをレンダリングする際、どのアイテムが変更になったのか特定する必要があるため、リストの各項目に一意な識別子となるkeyが必要です。
Todo型にidプロパティとして一意な数字(number 型)をもたせることにします。
また、書き換え不可能であるreadonly(読み取り専用)のプロパティとします。
type Todo = {
value: string;
+ readonly id: number;
};
Todo型オブジェクトにはidプロパティの指定が必須となったため、handleOnSubmit()メソッドを更新します。
const handleOnSubmit = () => {
if (text === "") {
alert("文字を入力してください");
return;
}
const newTodo: Todo = {
value: text,
+ id: new Date().getTime(),
};
setTodos([newTodo, ...todos]);
setText("");
};
<li></li>タグにkey (=id)を付加します。
<ul>
{todos.map((todo) => {
return <li key={todo.id}>{todo.value}</li>;
})}
</ul>
keyにindex番号を割り当てる時(非推奨)
以下のようにしてkeyにindex番号を割り当てることでも動きますが推奨されていないみたいです。以下のサイトに書いてあります。
<ul>
+ {todos.map((todo, index) => {
+ return <li key={index}>{todo.value}</li>;
})}
</ul>
タスクを追加することができました。
削除機能を追加する
追加したタスクを削除できるようにします。
Booleanのtrue、falseで削除/未削除を管理します。Todo 型に追加します。
type Todo = {
value: string;
readonly id: number;
+ removed: boolean;
};
handleOnSubmit()メソッドを更新します。
const newTodo: Todo = {
value: text,
id: new Date().getTime(),
+ removed: false,
};
それぞれの入力フォームの後ろへ削除ボタンを追加します。
また、すでに削除済みかどうか可視化するため、todo.removedの値によってラベルを入れ替えます。
return (
<li key={todo.id}>
{todo.value}
+ <button onClick={() => handleOnRemove(todo.id, todo.removed)}>
+ {todo.removed ? "復元" : "削除"}
+ </button>
</li>
);
削除ボタンがクリックされたときのコールバック関数を作成します。
+ const handleOnRemove = (id: number, removed: boolean) => {
+ const deepCopy: Todo[] = JSON.parse(JSON.stringify(todos));
+ const newTodos = deepCopy.map((todo, todoIndex) => {
+ if (todo.id === id) {
+ todo.removed = !removed;
+ }
+ return todo;
+ });
+ setTodos(newTodos);
+ };
タスクをフィルタリングする機能を追加する
削除済みアイテムも一緒に表示されてしまうので、タスクをフィルタリングする機能を追加します。
return (
<div className="w-full mx-auto max-w-2xl my-6 px-3">
+ <select
+ defaultValue="all"
+ onChange={(e) => setFilter(e.target.value as Filter)}
+ >
+ <option value="all">すべてのタスク</option>
+ <option value="removed">ゴミ箱</option>
+ </select>
現在のフィルターを格納するfilterステイトを追加します。
フィルターの状態を表すFilter 型を作ります。
+ type Filter = "all" | "removed";
<option />タグの値をFilter 型のステイトとして保持します。
const App = () => {
const [text, setText] = useState('');
const [todos, setTodos] = useState<Todo[]>([]);
+ const [filter, setFilter] = useState<Filter>("all");
onChangeイベントが発火すると、filterステイトを更新するようにしています。
// e.target.value: string を Filter 型にキャストする
<select
defaultValue="all"
onChange={(e) => setFilter(e.target.value as Filter)}
>
<option value="all">すべてのタスク</option>
<option value="removed">ゴミ箱</option>
</select>
フィルタリング後のTodo 型配列をリスト表示します。
todos ステイト配列の表示方法を変化させる関数を作成します。
-
<ul></ul>タグの中で展開されているtodos ステイトをタグへ渡す前に加工します。 - 現在の
filter ステイトに応じてTodo 型配列の要素をフィルタリングします。
+ const filteredTodos = todos.filter((todo) => {
+ // filterステイトの値に応じて異なる内容の配列を返す
+ switch (filter) {
+ case "all":
+ // 削除されていないもの全て
+ return !todo.removed;
+ case "removed":
+ // 削除済みのもの
+ return todo.removed;
+ default:
+ return todo;
+ }
+ });
todoステイトを展開する<ul></ul>タグにフィルタリング済みのリストを渡すように書き換えます。
<ul>
- {todos.map((todo) => {
+ {filteredTodos.map((todo) => {
「ゴミ箱」が表示されている時は新しいタスクを追加できないように入力フォームを無効化します。
<input
className="border border-black"
type="text"
value={text}
+ disabled={filter === "removed"}
onChange={(e) => handleOnChange(e)} />
<input
type="submit"
value="追加"
+ disabled={filter === "removed"}
onSubmit={handleOnSubmit}
/>
「ゴミ箱を空にする」機能を追加する
フィルターで「ゴミ箱」のタスクリストを表示している時は、削除済みタスクを完全に消去できるように機能を追加します。
フィルターが「ゴミ箱」の場合は「ゴミ箱を空にする」ボタンを表示し、それ以外の時は従来の入力フォームを表示するようにします。
<option value="removed">ゴミ箱</option>
</select>
+ {filter === "removed" ? (
+ <button
+ onClick={handleOnEmpty}
+ disabled={todos.filter((todo) => todo.removed).length === 0}
+ >
+ ゴミ箱を空にする
+ </button>
+ ) : (
<form
onSubmit={(e) => {
e.preventDefault();
handleOnSubmit();
}}
>
<input
className="border border-black"
type="text"
value={text}
disabled={filter === "removed"}
onChange={(e) => handleOnChange(e)} />
<input
type="submit"
value="追加"
disabled={filter === "removed"}
onSubmit={handleOnSubmit}
/>
</form>
+ )}
三項演算子? 'trueの時' : 'falseの時' で評価しています。
入力フォームが描写される時はfilter === 'removed'という状態が発生しないので入力フォームからこれを削除します。
<input
className="border border-black"
type="text"
value={text}
- disabled={filter === "removed"}
onChange={(e) => handleOnChange(e)} />
<input
type="submit"
value="追加"
- disabled={filter === "removed"}
onSubmit={handleOnSubmit}
/>
「ゴミ箱を空にする」コールバック関数の作成します。
todos ステイト配列から、removedフラグが立っている要素を取り除きます。
+ const handleOnEmpty = () => {
+ const newTodos = todos.filter((todo) => !todo.removed);
+ setTodos(newTodos);
+ };
以上です。ありがとうございました。コード全文はこちらです。
**コード全文**
import { useState } from 'react';
type Todo = {
value: string;
readonly id: number;
removed: boolean;
};
type Filter = "all" | "removed";
const App = () => {
const [text, setText] = useState('');
const [todos, setTodos] = useState<Todo[]>([]);
const [filter, setFilter] = useState<Filter>("all");
// todosステイトを更新する関数
const handleOnSubmit = () => {
// 何も入力されていない時
if (text === "") {
alert("文字を入力してください");
return;
}
//新しい Todo を作成
const newTodo: Todo = {
value: text,
id: new Date().getTime(),
removed: false,
};
setTodos([newTodo, ...todos]);
//フォームへの入力をクリアする
setText('');
};
// formに入力更新された時更新する関数
const handleOnChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setText(e.target.value);
};
// 削除ボタンがクリックされた時更新する関数
const handleOnRemove = (id: number, removed: boolean) => {
const deepCopy: Todo[] = JSON.parse(JSON.stringify(todos));
const newTodos = deepCopy.map((todo) => {
if (todo.id === id) {
todo.removed = !removed;
}
return todo;
});
setTodos(newTodos);
};
// ゴミ箱を空にする時更新する関数
const handleOnEmpty = () => {
const newTodos = todos.filter((todo) => !todo.removed);
setTodos(newTodos);
};
// フィルター
const filteredTodos = todos.filter((todo) => {
switch (filter) {
case "all":
return !todo.removed;
case "removed":
return todo.removed;
default:
return todo;
}
});
return (
<div className="w-full mx-auto max-w-2xl my-6 px-3">
<select
defaultValue="all"
onChange={(e) => setFilter(e.target.value as Filter)}
>
<option value="all">すべてのタスク</option>
<option value="removed">ゴミ箱</option>
</select>
{filter === "removed" ? (
<button
onClick={handleOnEmpty}
disabled={todos.filter((todo) => todo.removed).length === 0}
>
ゴミ箱を空にする
</button>
) : (
<form
onSubmit={(e) => {
e.preventDefault();
handleOnSubmit();
}}
>
<input
className="border border-black"
type="text"
value={text}
onChange={(e) => handleOnChange(e)} />
<input
type="submit"
value="追加"
onSubmit={handleOnSubmit}
/>
</form>
)}
<ul>
{filteredTodos.map((todo) => {
return (
<li key={todo.id}>
{todo.value}
<button onClick={() => handleOnRemove(todo.id, todo.removed)}>
{todo.removed ? "復元" : "削除"}
</button>
</li>
);
})}
</ul>
</div>
);
};
export default App;




