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;