1 連絡
1.1 情報提供
- 「次の電車間に合う?」がわかる運行掲示板、学生が1万円で作った!大学に設置、職員にも好評@Yahoo!News
- 技術的には皆さんでも十分に開発可能だと思います。アイデアは積極的にカタチにして行動していきましょう。
1.2 ここまでの流れ
本科目は学修単位科目です。1回の授業あたり、皆さんが「4時間相当の授業時間外学習」をすることを前提 としたボリュームと展開速度となっています。1週間の間隔をあけての90分だけの取り組みで理解・習得できる内容ではないので注意してください。
初回授業から後期中間試験ぐらいまでの授業の流れ
- モダンTypeScript基礎学習のための環境構築 済
- TypeScript基礎学習 済
- React / Next.js 開発に関連する文法や機能だけを集中的に学びました。
- Reactを使ったTodoアプリのための環境構築 済
- Todoアプリ開発(Reactによるフロントエンド開発)のチュートリアル ← 今回の授業
- Todoアプリのカスタマイズや作り込み →
課題1
- 詳細はこちらを参照。
2 新規タスクの追加
現在のTodoリストに「新しいタスク (Todo) を追加する処理」を実装します。
src/App.tsx
が、次のようなコードになっていることを前提に解説します。もし、src/App.tsx
をカスタマイズしている場合は、一旦、退避させて以下のコードを使用して実験してください。
import { useState } from "react";
import { Todo } from "./types";
import { initTodos } from "./initTodos";
import WelcomeMessage from "./WelcomeMessage";
import TodoList from "./TodoList";
const App = () => {
const [todos, setTodos] = useState<Todo[]>(initTodos);
const uncompletedCount = todos.filter(
(todo: Todo) => !todo.isDone
).length; // 未完了タスクの数え上げ
return (
<div className="mx-4 mt-10 max-w-2xl md:mx-auto">
<h1 className="mb-4 text-2xl font-bold">TodoApp</h1>
<div className="mb-4">
<WelcomeMessage
name="寝屋川タヌキ"
uncompletedCount={uncompletedCount}
/>
</div>
<TodoList todos={todos} />
</div>
);
};
export default App;いまから、このページの下部に「追加」というボタンを作成します。そして、ユーザーがそのボタンを押下したときに…
- 新しいTodoオブジェクト
newTodoを生成する。 - 現在のTodo配列
todosの末尾にnewTodoを追加した新しいTodo配列newTodosを生成する。- ここでイミュータブルな配列操作を使うことがポイントです。
- 状態を変更する関数
setTodosにnewTodosを与える (React的な作法に則って状態を更新)setTodosは上記プログラムの 第08行目 でuseStateによって生成されたものです。
以上の手順でTodoリストを更新し、適切に画面が再レンダリング されるように
src/App.tsx
をアップデートしていきます。具体的には、次のように実装します。
import { useState } from "react";
import { Todo } from "./types";
import { initTodos } from "./initTodos";
import WelcomeMessage from "./WelcomeMessage";
import TodoList from "./TodoList";
import { v4 as uuid } from "uuid"; // ◀◀ 追加
const App = () => {
const [todos, setTodos] = useState<Todo[]>(initTodos);
const uncompletedCount = todos.filter(
(todo: Todo) => !todo.isDone
).length;
// 追加: addNewTodo関数の実装
const addNewTodo = () => {
const newTodo: Todo = {
id: uuid(),
name: "新しいタスク",
isDone: false,
priority: 3,
deadline: new Date(2024, 10, 13),
};
// スプレッド構文を使って、末尾に新タスクを追加した配列を作成
const updatedTodos = [...todos, newTodo];
setTodos(updatedTodos); // 作成した配列をtodosにセット
};
return (
<div className="mx-4 mt-10 max-w-2xl md:mx-auto">
<h1 className="mb-4 text-2xl font-bold">TodoApp</h1>
<div className="mb-4">
<WelcomeMessage
name="寝屋川タヌキ"
uncompletedCount={uncompletedCount}
/>
</div>
<TodoList todos={todos} />
{/* タスク追加関連のUI実装 ここから... */}
<div className="mt-5 space-y-2 rounded-md border p-3">
<h2 className="text-lg font-bold">新しいタスクの追加</h2>
<button
type="button"
onClick={addNewTodo} // ボタンを押下したときの処理
className="rounded-md bg-indigo-500 px-3 py-1 font-bold text-white hover:bg-indigo-600"
>
追加
</button>
</div>
{/* ...ここまで */}
</div>
);
};
export default App;上記のプログラムでは、新しいタスクを追加するための「addNewTodo」という関数を
第15~26行
にアロー関数形式で実装しています。JavaScript (TypeScript) では、クロージャという仕組みによって
「関数のなかに定義される関数の内部で、親となる関数が持つ変数にアクセスすることが可能」
となっています。ここで「関数
addNewTodo」は「関数App」の内部で定義されているため、第24行目
のように addNewTodo関数の外側にある変数
todos
の参照が可能になっています。なお、Python
にもクロージャの仕組みがありますが、C言語ではクロージャは利用できません
(そもそもC言語では、関数内部に関数を定義することができません)。
UIに関連したものとしては
第40行目~第49行目
にJSX要素を追加しています。特に 第44行目 の
onClick={addNewTodo}
によって、ボタンがクリックしたときに 第15行目
の「addNewTodo関数」が呼び出されるようになっている点に注目してください。なお、ここで
onClick={addNewTodo()}
とすると間違いが多いので注意してください。もし、両者の違いが分からないときは生成AIを使って解決してください。
(プロンプト例)
TypeScript環境のReactで「Todoアプリ」を開発しています。ボタンを押下したとき に
addNewTodoという関数 (引数なし) を実行したいです。このようなときは、ボタンの属性をonClick={addNewTodo}のように設定すると習いました。なぜ、onClick={addNewTodo()}のように書かないのですか?
2.1 動作確認
src/App.tsx を更新・保存して、開発モード
(npm run dev)
でアプリを実行してください。以下の図ように「追加」ボタンを押下するたびに、リスト末尾に「新しいタスク」が追加されていくことが確認できます。
※ 実際の実行画面 (Todoリストの外観)
は「TodoListコンポーネント
(src/TodoList.tsx)」の実装によって変化します。前回講義の宿題として、TodoListコンポーネントをカスタマイズしてもらっているので、実行画面の外観は各々で異なるはずです。
※ Todoリストの初期値 (src/initTodos.ts) は こちらを使用しています。
注意
前回講義の演習の一環で、TodoList.tsx
の内部で次のように todos の
並び替え処理 を実装している場合は、、、
一時的に、並び替えを無効にしておいてください (initTodosの順番に表示されるようにしておいてください)。
2.1.1 演習 3分
- 新規タスクをTodoリストの「末尾」ではなく「先頭」に追加するように変更してください。
- 答え
const updatedTodos = [...todos, newTodo]をconst updatedTodos = [newTodo, ...todos]に変更
- 答え
確認後は、元に戻しておいてください (「末尾」に新規タスクを追加するように戻しておいてください)。
2.2 NG: Todos配列に対するミュータブルな操作
理解を深めるためにミュータブルな配列操作を使って
todos を更新するとどうなるか?
について実験・確認しておきます。ミュータブルな操作を使って
addNewTodo を実装した例を以下に示します。特に
第09行目 に着目してください。
const addNewTodo = () => {
const newTodo: Todo = {
id: uuid(),
name: "新しいタスク",
isDone: false,
priority: 3,
deadline: new Date(2024, 10, 13),
};
todos.push(newTodo); // ミュータブルな配列操作 => ReactではNG
setTodos(todos);
console.log(JSON.stringify(todos, null, 2)); // todosの内容を表示
};上記のように addNewTodo
を実装すると、「追加」のボタンを押下しても ウェブ画面上にはタスクが追加されない
ことが確認できます。ただし、第11行目
で行っているのコンソール出力を デベロッパーツール
([F12] で起動) から確認すると、以下のように
内部的には todos
の末尾に新しいタスクが正常に追加されていることが確認できます。
つまり、push メソッドなどの
ミュータブルな配列操作 で変更した配列を
setTodos に与えても React
は画面の再レンダリングはしない
ということが確認できました。React は setTodos
の引数に与えた オブジェクトの参照
(C言語で言えばポインタ、Pythonで言えばオブジェクトID)
が、前回から変化しているときに
変更が生じたと判断して
(=変更を検知して)
画面を再レンダリングする、という仕組みなっていることを覚えておいてください。
3 新規タスクの名前設定テキストボックスの追加
新規タスクの名前 (=Todoオブジェクトの
name プロパティ)
を設定するための「テキストボックス」を追加します。参考動画
(=1から簡単なTodoアプリを作ってReactの1歩を踏み出してみよう)
においては、テキストボックス関連の処理を 「useRef」という React Hook
で実装していましたが、ここでは、あとから
バリデーション
などの処理を追加するために「useState」を使って実装します。
なお、アプリ開発の文脈において「バリデーション (Validation)」とは データの書式や内容のチェック、例えば 文字数が規定範囲内に収まっているか、文字列が有効なメールアドレス形式になっているかどうか といったチェック処理を指します。
以下に、新規タスクの名前 (name)
を設定するための「テキストボックス」を実装した例を示します。
import { useState } from "react";
import { Todo } from "./types";
import { initTodos } from "./initTodos";
import WelcomeMessage from "./WelcomeMessage";
import TodoList from "./TodoList";
import { v4 as uuid } from "uuid";
const App = () => {
const [todos, setTodos] = useState<Todo[]>(initTodos);
const [newTodoName, setNewTodoName] = useState(""); // ◀◀ 追加
const uncompletedCount = todos.filter(
(todo: Todo) => !todo.isDone
).length;
console.log(JSON.stringify(todos, null, 2));
// ▼▼ 追加
const updateNewTodoName = (e: React.ChangeEvent<HTMLInputElement>) => {
// あとでバリデーションなどの処理をここに追加する
setNewTodoName(e.target.value);
};
const addNewTodo = () => {
const newTodo: Todo = {
id: uuid(),
name: newTodoName, // ◀◀ 編集
isDone: false,
priority: 3,
deadline: new Date(2024, 10, 13),
};
const updatedTodos = [...todos, newTodo];
setTodos(updatedTodos);
setNewTodoName(""); // ◀◀ 追加
};
return (
<div className="mx-4 mt-10 max-w-2xl md:mx-auto">
<h1 className="mb-4 text-2xl font-bold">TodoApp</h1>
<div className="mb-4">
<WelcomeMessage
name="寝屋川タヌキ"
uncompletedCount={uncompletedCount}
/>
</div>
<TodoList todos={todos} />
<div className="mt-5 space-y-2 rounded-md border p-3">
<h2 className="text-lg font-bold">新しいタスクの追加</h2>
{/* テキストボックスの実装 ここから... */}
<div className="flex items-center space-x-2">
<label className="font-bold" htmlFor="newTodoName">
名前
</label>
<input
id="newTodoName"
type="text"
value={newTodoName}
onChange={updateNewTodoName}
className="grow rounded-md border p-2"
placeholder="2文字以上、32文字以内で入力してください"
/>
</div>
{/* ...ここまで */}
<button
type="button"
onClick={addNewTodo}
className="rounded-md bg-indigo-500 px-3 py-1 font-bold text-white hover:bg-indigo-600"
>
追加
</button>
</div>
</div>
);
};
export default App;ウェブアプリにおいて「テキストボックス」や「チェックボックス」「ラジオボタン」など入力系UI
(フォーム入力) には「<input> タグ」を利用します
(参考: フォーム入力の種類)。そして、React開発においては、一般に
<input> タグは useRef または useState
を組み合わせて使用 します。
ここでは「新規タスクの名前」をテキストボックスから得たいので、第10行目
において useState から「変数
newTodoName」と「関数
setNewTodoName」 を生成しています (つまり
newTodoName
を「状態」として扱うように実装しています)。また
第10行目 では newTodoName の初期値を
空文字 ""
に設定しています。なお、第10行目 は
useState<string>("");
のように型を明示して初期化することも可能です
(型推論が可能なときに型を省略するか、明示するかは (個人開発では)
自由に決めてください)。
3.1 ユーザによるUI操作と状態更新の対応付け
第57行目 では、この newTodoName
の内容がテキストボックスに反映されるように
<input> タグの value属性 を
value={newTodoName} のように設定しています。
ただし、これだけでは逆方向の値の反映がされません。具体的には
ユーザによるテキストボックスの編集操作が、変数
newTodoName に対して
反映されません。これを反映するには、まず 第58行目
のように onChange={updateNewTodoName}
とする必要があります。これにより、テキストボックスで文字列を編集するたびに、第17行目
で定義している「updateNewTodoName関数」がコールされるようになります
(呼び出されるようになります)。この際、コールされる関数には「React.ChangeEvent<HTMLInputElement>
型の引数」が渡されます。この引数には、型の名前が表すように HTML の Input要素
の変更イベントに関する各種情報 が格納されています。
第17行目
の「updateNewTodoName関数」では、それを e
という仮引数で受け取って e.target.value
から「変更された文字列」を取得して、第19行目
でsetNewTodoName にセットしています。
3.1.1 演習
第59行目 のCSSクラスから grow
を削除するとテキストボックスのスタイリングがどのように変化するか確認してください。
3.2 ラベルタグの設定
ここでの実装例では、テキストボックス (インプット要素) に対して
ラベルタグ <label>
を組み合わせて使用しています。ラベルタグの
htmlFor属性 に対して「紐づけたいインプット要素の
id」を設定すると (例えば、第51行目
のように htmlFor="newTodoName" のように設定すると)
ラベルをクリックしたときに 対応するインプットがアクティブ
になります。
<div className="flex items-center space-x-2">
<label className="font-bold" htmlFor="newTodoName">
名前
</label>
<input
id="newTodoName"
type="text"
value={newTodoName}
onChange={updateNewTodoName}
className="grow rounded-md border p-2"
placeholder="2文字以上、32文字以内で入力してください"
/>
</div>些細な工夫ですが、確実に UI/UX の向上に繋がるので
<input> と <label> は
セットで使用すること
を意識するようにしてください。
4 優先度を設定するラジオボタンの追加
新規タスクの優先度 (priority)
を設定するための「ラジオボタン」を実装する例を以下に示します。
ラジオボタンには、第76行目 のように属性を
type="radio" のように設定した
<input> タグを使用します。
import { useState } from "react";
import { Todo } from "./types";
import { initTodos } from "./initTodos";
import WelcomeMessage from "./WelcomeMessage";
import TodoList from "./TodoList";
import { v4 as uuid } from "uuid";
const App = () => {
const [todos, setTodos] = useState<Todo[]>(initTodos);
const [newTodoName, setNewTodoName] = useState("");
const [newTodoPriority, setNewTodoPriority] = useState(3); // ◀◀ 追加
const uncompletedCount = todos.filter(
(todo: Todo) => !todo.isDone
).length;
const updateNewTodoName = (e: React.ChangeEvent<HTMLInputElement>) => {
// あとでバリデーションなどの処理をここに追加する
setNewTodoName(e.target.value);
};
// ▼▼ 追加
const updateNewTodoPriority = (e: React.ChangeEvent<HTMLInputElement>) => {
setNewTodoPriority(Number(e.target.value)); // 数値型に変換
};
const addNewTodo = () => {
const newTodo: Todo = {
id: uuid(),
name: newTodoName,
isDone: false,
priority: newTodoPriority, // ◀◀ 追加
deadline: new Date(2024, 10, 13),
};
const updatedTodos = [...todos, newTodo];
setTodos(updatedTodos);
setNewTodoName("");
setNewTodoPriority(3); // ◀◀ 追加
};
return (
<div className="mx-4 mt-10 max-w-2xl md:mx-auto">
<h1 className="mb-4 text-2xl font-bold">TodoApp</h1>
<div className="mb-4">
<WelcomeMessage
name="寝屋川タヌキ"
uncompletedCount={uncompletedCount}
/>
</div>
<TodoList todos={todos} />
<div className="mt-5 space-y-2 rounded-md border p-3">
<h2 className="text-lg font-bold">新しいタスクの追加</h2>
<div className="flex items-center space-x-2">
<label className="font-bold" htmlFor="newTodoName">
名前
</label>
<input
id="newTodoName"
type="text"
value={newTodoName}
onChange={updateNewTodoName}
className="grow rounded-md border p-2"
placeholder="2文字以上、32文字以内で入力してください"
/>
</div>
{/* ラジオボタンの実装 ここから... */}
<div className="flex gap-5">
<div className="font-bold">優先度</div>
{[1, 2, 3].map((value) => (
<label key={value} className="flex items-center space-x-1">
<input
id={`priority-${value}`}
name="priorityGroup"
type="radio"
value={value}
checked={newTodoPriority === value}
onChange={updateNewTodoPriority}
/>
<span>{value}</span>
</label>
))}
</div>
{/* ...ここまで */}
<button
type="button"
onClick={addNewTodo}
className="rounded-md bg-indigo-500 px-3 py-1 font-bold text-white hover:bg-indigo-600"
>
追加
</button>
</div>
</div>
);
};
export default App;はじめに 第11行目
で、新規タスクの優先度を「状態」として管理するための
newTodoPriority と setNewTodoPriority
を用意していることを確認してください。また、その初期値として 「数値型」の 3
を設定していることを確認してください。
そして、ラジオボタンが操作されたタイミングで
第23行目 で定義した
updateNewTodoPriority がコールされて、そのなかで
setNewTodoPriority
で状態が更新される仕組みになっていることを確認してください。なお、e.target.value
で取得される値は <input>タグの種別や属性設定に関係なく常に「文字列型」になること
に注意してください。そのため 第24行目
では、Number
関数を使って「文字列型」から「数値型」に変換して
setNewTodoPriority にセットしています。
Number関数を使用していないと、次のようにエラーとなります。
JSX部分の「ラジオボタンの実装」は、「テキストボックスの実装」と比較するとかなり複雑になっています。次に、以下の点を意識しながら 第69行目 ~ 第84行 のコードを読解してください。
- 前回講義で解説したようにJSX構文のなかで
mapを利用するときは map の戻り値となる最外殻の要素には「key属性」 を設定する必要がありました。そのため 第72行目 に「key属性」を設定しています。- key属性を設定しないとブラウザのコンソールに「Warning: Each child in a list should have a unique “key” prop.」のような警告が表示されます。
- テキストボックスを実装したときは
<label>と<input>を並列に配置しました (いわゆる「兄弟」の関係になるように配置しました) 。一方で、ここでは<input>が<label>の子要素 になるように配置 (いわゆる「親子」の関係になるように配置)しています。このように<input>を<label>の子要素とすると 「htmlFor属性」を設定せずに両者の紐づけが可能になります。- (ラジオボタンの本体ではなく)「1」「2」「3」という文字列のクリックすることでも 対応するラジオボタンを選択できること を確認してください。
- ラベルとインプットの対応付けという意味では
第74行目
の「id属性」は不要です。ただし、一般にインプット要素には「id属性」を与えることが作法であるため、ここでは
id={`priority-${value}`}のように設定しています。
- ラジオボタンでは
「name属性が同じもの」がグループ化 されて、そのグループのなかで1個のボタンだけが選択できる
ようになります。そのため 第75行目
では各ボタンとも共通で
name="priorityGroup"を設定しています。ただし、この実装ではonChangeとcheckedで選択を制御しているため、「name属性」を設定していなくともラジオボタンとしての動作をします。ただし、ここでも一般的な作法として「name属性」を設定しています。
4.1 補足: onChange属性の設定
第23行目 の関数
updateNewTodoPriority
を定義せずに、第79行目 の
onChange に 「アロー関数形式」で「優先度を設定する処理」を直接的に記述すること
も可能です。
<input
id={`priority-${value}`}
name="priorityGroup"
type="radio"
value={value}
checked={newTodoPriority === value}
onChange={(e) => updateNewTodoPriority(e)} // ◀◀ 注目
/>上記のようなテクニックは、Reactでは頻繁に使われるので覚えておいてください。特に処理が短い場合は、こちらの記法が推奨されます。また、ここでは型推論が有効になるので、以下のように長々と「型」を記述しなくても良いことも嬉しいです。
4.2 補足: mapを使用せずにラジオボタンを構成する例
map を使用せずに 第69行目 ~
第84行目 のJSXを記述すると
(ラジオボタンを実装すると) 次のようになります。
<div className="flex gap-5">
<div className="font-bold">優先度</div>
<label className="flex items-center space-x-1">
<input
id="priority-1"
name="priorityGroup"
type="radio"
value="1"
checked={newTodoPriority === 1}
onChange={updateNewTodoPriority}
/>
<span>1</span>
</label>
<label className="flex items-center space-x-1">
<input
id="priority-2"
name="priorityGroup"
type="radio"
value="2"
checked={newTodoPriority === 2}
onChange={updateNewTodoPriority}
/>
<span>2</span>
</label>
<label className="flex items-center space-x-1">
<input
id="priority-3"
name="priorityGroup"
type="radio"
value="3"
checked={newTodoPriority === 3}
onChange={updateNewTodoPriority}
/>
<span>3</span>
</label>
</div>重複する部分が多いので map
を使ってJSXを構成するほうがスマートであることが分かると思います。
4.3 ラジオボタンのラベルに任意の値を使用する方法
ラジオボタンの「ラベル」を次のように任意の値 (例えば 高・中・低 など) を設定したいときの実装例を示します。
このようにするためには、JSXを次のように変更します。[1,2,3]
を ["高", "中", "低"]
に変えて、その他、いくつかの属性を変えています。
<div className="flex gap-5">
<div className="font-bold">優先度</div>
{["高", "中", "低"].map((value, index) => ( // ◀◀ 注目 index を追加
<label key={value} className="flex items-center space-x-1">
<input
id={`priority-${value}`}
name="priorityGroup"
type="radio"
value={value}
checked={newTodoPriority === index + 1} // ◀◀ 注目
onChange={updateNewTodoPriority}
/>
<span>{value}</span>
</label>
))}
</div>さらに、上記の変更にあわせて関数
updateNewTodoPriority
を次のようにします。上記の変更によって e.target.value
の値が 高、中、低
のいずれかになるので、それを switch
で振り分けています。
const updateNewTodoPriority = (e: React.ChangeEvent<HTMLInputElement>) => {
switch (e.target.value) {
case "高":
setNewTodoPriority(1);
break;
case "中":
setNewTodoPriority(2);
break;
case "低":
setNewTodoPriority(3);
break;
}
};なお、上記の以外にも value={value} を
value={index+1}
のようにしてしまうスマートな方法もあります (この方法では、そもそも
updateNewTodoPriority
を変更する必要がありません)。
5 期限を設定する日時設定UIの追加
新規タスクの期限 (deadline)
を設定するための「日時設定UI」を実装する例を以下に示します。
日時設定UIには、属性を type="datetime-local"
に設定した <input>
タグを使用します。なお、Chrome において「2024/10/31 ()
17:20」のように括弧の内部が表示されないのは仕様
(バグ?) です。
- 新規タスクの期限設定を実装した
src/App.tsxの全体のコードはこちらを参照してください。
5.1 日時情報の更新 (UI→コード)
前回講義で仕様を決めたように「特に期限を設定しないTodo」については
deadline を
null として扱うものとします。
export type Todo = {
id: string;
name: string;
isDone: boolean;
priority: number;
deadline: Date | null; // ◀◀ 注目
};そのため、第13行目 (全体コード参照)
のように、期限を扱う「状態」については
useState<Date | null> のように
型を明示して初期化
する必要があります。このように、値が「Date型」または「null」のどちらかになり得るような型
を Union型 (ユニオン型)
とよびます。Union型では 初期値から型推論が不可能
であるため明示的に型を示す必要があります。
もし、ここで次のように 型を指定せずに初期化
してしまうと、newTodoDeadline は null型の変数 (=値として null
だけを持つことができる変数)
になってしまうので注意してください。
つづいて 第28行目 からは、以下のように 日時設定UIでユーザにより値が変更されたとき にコールされる「updateDeadline関数」を定義しています。
const updateDeadline = (e: React.ChangeEvent<HTMLInputElement>) => {
const dt = e.target.value; // UIで日時が未設定のときは空文字列 "" が dt に格納される
console.log(`UI操作で日時が "${dt}" (${typeof dt}型) に変更されました。`);
setNewTodoDeadline(dt === "" ? null : new Date(dt));
};日時設定UIを操作して日時を選択すると
e.target.value からは 「2024-11-30T15:30」のようなISO8601形式の文字列
が得られます。実際に 第30行目
のコンソール出力の内容を確認してください。また、以下の図のように日時設定UIで「削除」を選択したときは
e.target.value からは 空文字
("") が得られます。
このような「2024-11-30T15:30形式の文字列」もしくは「空文字」を、Date | null
型の deadline
にセットするために、第31行目 では
dt === "" ? null : new Date(dt) のように
条件演算子 を使った変換をしています。条件演算子
(三項演算子) については第02回講義で学習済みです。
これにより、dt === ""
が「真」となるとき、newTodoDeadline には
null がセットされます。また、そうでないときは new Date("2024-11-30T15:30")
で生成される「Data型オブジェクト」が
newTodoDeadline にセットされます。Dateクラスは
"2024-11-30T15:30"
形式の文字列を使った初期化にも対応しています
(ただし、ブラウザ互換性などを考慮して、より安全に使用するためには
new Date(Date.parse(dt))
とすることが推奨されます)。
なお、空文字 "" は、単体で false
として評価されるため (空文字は「falsyな値」であるため)、第31行目は、次のように記述することもできます。
5.2 日時情報の更新 (コード→UI)
上記とは逆に、Date | null 型の
deadline から、日時設定UI (日時設定フォーム)
に値を反映させるために、 第101行目
で以下のように工夫を行なって value属性
を設定しています。
<input
type="datetime-local"
id="deadline"
value={
newTodoDeadline
? dayjs(newTodoDeadline).format("YYYY-MM-DDTHH:mm:ss")
: ""
}
onChange={updateDeadline}
className="rounded-md border border-gray-400 px-2 py-0.5"
/>null は単体で false の扱いになるため
(null
は「falsyな値」であるため)、newTodoDeadline が
null のときは、value属性に「空文字 ""
」がセットされます。
一方で newTodoDeadline が null
以外のときは、dayjs
を利用してvalue属性に「ISO8601形式
"YYYY-MM-DDTHH:mm:ss"
に変換された文字列」がセットされるようにしています (注意:
ここでは「秒」まで指定しないと Safari
では意図した動作をしない可能性があります)。
このように「日時」に関しては、コードとUIの間で双方向の型変換 が必要となり、ブラウザによる挙動の違い (対応している日時フォーマットの違い) やタイムゾーン、使用するライブラリを十分に考慮しながら慎重に実装する必要があります。
5.3 「truthyな値」と「falsyな値」
JavaScript / TypeScript において、条件式
(if文の条件部分や条件演算子の条件部分など) で
論理値として評価されるとき
に、true
と解釈される値を「truthyな値」、false
と解釈される値を「falsyな値」といいます。
「falsyな値」は
false、""(空文字)、null、undefined、0、-0、0n、NaNで、それ以外は
「truthyな値」となります。
特に注意すべきは、[] (空配列) や、{}
(空オブジェクト)、"0" (文字列の0) で、これらは truthyな値 となります。
- 参考:
0nはBigIntという非常に大きな整数 (Number型の範囲を超えるような値) を扱うためのデータ型における「0」です。
5.3.1 定着確認
定数 hoge に代入される値を答えよ。
const hoge = "" ? "truthyな値" : "falsyな値";- 答え
"falsyな値"・偽と解釈
- 答え
const hoge = 0.000 ? "truthyな値" : "falsyな値";- 答え
"falsyな値"・偽と解釈
- 答え
const hoge = "0" ? "truthyな値" : "falsyな値";- 答え
"truthyな値"・真と解釈
- 答え
const hoge = null ? "truthyな値" : "falsyな値";- 答え
"falsyな値"・偽と解釈
- 答え
const hoge = [] ? "truthyな値" : "falsyな値";- 答え
"truthyな値"・真と解釈
- 答え
const hoge = [false] ? "truthyな値" : "falsyな値";- 答え
"truthyな値"・真と解釈
- 答え
確認にはpaiza.ioなどを利用してください。
6 バリデーション
現状では、新規タスクの「名前」のテキストボックスが空欄であっても、「追加」のボタンを押下すればタスクが追加できてしまいます。ここでは「2文字以上、32文字以内でテキストボックスに名前が入力されていないとき」はタスクが追加できないように設定していきます。
単純には「addNewTodo関数」の先頭に、次のような処理 (第02行目~第04行目) を追加すれば、この処理が可能になります。実際に試してみてください。
const addNewTodo = () => {
if (newTodoName.length < 2 || newTodoName.length > 32) {
return;
}
const newTodo: Todo = {
id: uuid(),
name: newTodoName,
isDone: false,
priority: newTodoPriority,
deadline: newTodoDeadline,
};
const updatedTodos = [...todos, newTodo];
setTodos(updatedTodos);
setNewTodoName("");
setNewTodoPriority(3);
setNewTodoDeadline(null);
};上記の処理によって、「追加」のボタンを押下しても
newTodoName
が条件を満たしていなければタスクが追加できないようになりました。しかし、このままでは
UI/UX 的には大きな課題が残ります
(ユーザが「なぜタスクが追加できないのか」に気づきづらい不親切な
UI/UX になっている)。
そこで、条件を満たしていないときは、以下のように画面上に注意喚起のメッセージを表示するようにアプリを改良していきます。このために
newTodoNameError という 文字列型の変数
(状態)
を導入します。そして、テキストボックスに入力されている値が適切であれば、この
newTodoNameError には空文字 ""
が格納し、そうでないときは 不備を指摘する文字列
を格納するようにします。また、newTodoNameError
を参照して、不備があるときは 「追加」ボタンが押下できない
ようにします。
- このバリデーションを実装した
src/App.tsxの全体のコードはこちらを参照してください。まずは、コピペして動作を確認することからはじめたほうが分かりやすいと思います。
特に 第81行目 から 第110行目 にかけて大幅に書き換えをしています (その他の部分についても編集や追記があるので注意してください)。
{/* 編集: ここから... */}
<div>
<div className="flex items-center space-x-2">
<label className="font-bold" htmlFor="newTodoName">
名前
</label>
<input
id="newTodoName"
type="text"
value={newTodoName}
onChange={updateNewTodoName}
className={twMerge(
"grow rounded-md border p-2",
newTodoNameError && "border-red-500 outline-red-500"
)}
placeholder="2文字以上、32文字以内で入力してください"
/>
</div>
{newTodoNameError && (
<div className="ml-10 flex items-center space-x-1 text-sm font-bold text-red-500 ">
<FontAwesomeIcon
icon={faTriangleExclamation}
className="mr-0.5"
/>
<div>{newTodoNameError}</div>
</div>
)}
</div>
{/* ...ここまで */}このコードのなかで特に注意してほしいのは
第99行目 の論理演算子 &&
を使った処理になります。この処理は、React
では頻繁に使用されるテクニックで 条件付きレンダリング と呼ばれます。
React のJSX構文のなかで、例えば
{ hoge && (<div>こんにちは</div>)}
というコードを書くと、変数 hoge が
false、null、undefined
のいずれかのときは、そこに何もレンダリングされません
(つまり {...}
の部分が存在していないのと同等の扱いとなります)。
一方で、hoge が truthyな値
のときは <div>こんにちは</div>
がレンダリングされます。ただし、falseな値 でも
0 と ""
は例外的に、以下のようになるので注意してください。
hogeが0のときは、数値の「0」がレンダリング されます。hogeが空文字""のときは空文字がレンダリングされます(画面上には何も表示されませんが、DOM上には存在します)
同様に、第94行目
でもこのテクニックを使用しています。twMerge は前回講義でインストールした
tailwind-merge ライブラリ
から、以下のようにインポートしている関数です。この
twMerge は Tailwind CSS
のクラスを衝突を回避しながら結合する機能を持った関数
となります。
第92行目の
twMerge("grow rounded-md border p-2", newTodoNameError && "border-red-500 outline-red-500")}
は、newTodoNameError が「空文字
"" のとき」は twMerge("grow rounded-md border p-2")
と等価になります。
そうでないときは
twMerge("grow rounded-md border p-2", "border-red-500 outline-red-500")}
と等価になります。つまり、新規タスクの名前に不備があるとき
(文字数が 2文字以上32文字以内になっていないとき) テキストボックスの枠を赤線にするクラスを追加する仕組み
をつくっています。
twMerge の必要性については Tailwind
CSS初心者が絶対ハマる落とし穴@ ムーザルちゃんねる (動画)
を参考にしてください。
6.1 補足: React Hook Form と Zod
Reactにおいて、テキストボックスやラジオボタンなどを用いた フォーム処理 にはReactHookFormというライブラリが活用できます。このTodoアプリのように入力項目が少ない場合は、現状のように useState を使ったシンプルな実装で十分ですが、フォームの規模が大きくなるほど ReactHookFormの導入効果 が高まります。
また、フォームのバリデーションにはzodというライブラリが活用できます。これは、ReactHookFormと組み合わせて使用可能であり、以下ような型安全なバリデーションルールを宣言的に記述できます。TypeScriptとの相性も良く、入力値の型推論も自動的に行われるため、開発効率の向上が期待できます。
- 必須項目のチェック
- 文字数制限や数値範囲の検証
- メールアドレスや電話番号などの形式チェック
- カスタムバリデーションルールの追加
フロントエンド開発を目指す人は、これらのライブラリを積極的に利用して課題1に取り組んでください。
7 既存Todoの 完了/未完了 の切り替え
既存の Todo について isDone の
true/false
を切り替える機能を実装します。ここでは、次のように
チェックボックス と紐づけて isDone
の値 (つまり完了/未完了) を切り替えるようにしていきます。
ここからは意図的に App.tsx
と TodoList.tsx
の全体コードを提供しません。ここまでの内容を十分に理解し、以降で提供されるコードの断片を「自分のコードのどこに配置すればよいか?」を考えながら作業を進めてください。
まずは、src/App.tsx のなかに、次のような
updateIsDone 関数を追加します。
この関数は、引数で受け取った id と一致する Todo
について、その isDone プロパティの値を引数
value の値で上書きするものです。例えば
updateIsDone("a001",false)
のように関数をコールしたら、id が a001
のTodoの isDone を false
に更新する機能を持たせます。特に、todos
が変更されたことを React が確実に検知できるように
「map」と「スプレッド構文」を使ってイミュータブルな操作
で処理するように実装する必要があります。
- スプレッド構文によるイミュータブルなオブジェクト更新@第03回講義
次の updateIsDone を src/App.tsx
のなかの 適切な位置 に追記してください。
const updateIsDone = (id: string, value: boolean) => {
const updatedTodos = todos.map((todo) => {
if (todo.id === id) {
return { ...todo, isDone: value }; // スプレッド構文
} else {
return todo;
}
});
setTodos(updatedTodos);
};次に src/TodoList.tsx
において、各Todoの情報を表示している UI に
チェックボックス を追加し、そこに
checked={todo.isDone}
の属性を設定して「完了/未完了の状態
(isDoneプロパティ)」と「チェックボックスのオンオフ」を連動させます。
以下のコードを参考に、src/TodoList.tsx
のなかでTodoの情報を表示している UI
に「チェックボックス」を追加してください。
<div>
{todos.map((todo) => (
{/* 略 */}
<input
type="checkbox"
checked={todo.isDone} // ◀◀ 注目
className="mr-1.5 cursor-pointer"
/>
{/* 略 */}
)}
</div>これを実装すると、次のように Todo の UI に
チェックボックス がついて、isDone
の値 (初期値) を反映した オン/オフ
がつきます。ただし、現状では、まだ チェックボックスをクリックしてもオンオフが変化
しません。
デベロッパーツールを確認すると、コンソール出力には「You provided a ‘checked’ prop to a form field without an ‘onChange’ handler. This will render a read-only field. If the field should be mutable use ‘defaultChecked’. Otherwise, set either ‘onChange’ or ‘readOnly’.」 という警告が表示されています。
チェックボックスのクリックによって、当該Todoの
isDone
プロパティを変更して、UI表示も更新するためには、先に定義した「updateIsDone関数」をコールする
必要があります。
ただし、updateIsDone 関数は Appコンポーネント
(src/App.tsx)
の内部に定義しているため、src/TodoList.tsx
からは直接的にコールすることはできません。このようなとき、React
では Props
を使って親コンポーネントから子コンポーネントに「関数」を渡す
ということで解決します。
- 「Props (プロップス) って何?」という人は前回講義の内容の復習からはじめてください。
まずは、src/TodoList.tsx において、Props 経由で
updateIsDone を受け取れるように
Propsの型定義 を次のように変更します。これにより
文字列型と真偽値型の引数を持ち、戻り値がない関数を
updateIsDone という名前
で受け取れるようになります。
また、ユーザによってチェックボックスが操作されたタイミングで、Props
経由で受け取った updateIsDone
をコールするように、チェックボックスの「onChange属性」を次のように設定します。ここでは、先に紹介したようにonChangeにアロー関数形式で直接的に処理を記述しています。
<input
type="checkbox"
checked={todo.isDone}
onChange={(e) => props.updateIsDone(todo.id, e.target.checked)}
className="mr-1.5 cursor-pointer"
/>最後に src/App.tsx
のなかで、TodoListコンポーネントに対して「updateIsDone関数」を渡すように変更します。
以上で、チェックボックスを操作して「既存のTodoの完了/未完了を切り替えること」が可能になりました。内部的にも
todos のなかの当該Todoの isDone
プロパティが変更されています。
7.1 NG実装:ミュータブルな操作でオブジェクト配列の要素を変更
上記の解説では updateIsDone
を次のようにスプレッド構文を使用して
イミュータブルな操作
で処理するように実装しました。
const updateIsDone = (id: string, value: boolean) => {
const updatedTodos = todos.map((todo) => {
if (todo.id === id) {
return { ...todo, isDone: value }; // スプレッド構文
} else {
return todo;
}
});
setTodos(updatedTodos);
};これを次のようにミュータブルな操作で処理してしまうと意図するように機能しなくなります。実際に書き換えて確かめてください。
const updateIsDone = (id: string, value: boolean) => {
for (const todo of todos) {
if (todo.id === id) {
todo.isDone = value; // オブジェクトを直接変更
}
}
setTodos(todos);
};これは繰返し説明しているように、Reactでは画面の再レンダリングが「参照の変更を検知すること」で実行されるためです。上記のような処理では
setTodos(todos) をコールしても、引数の
todos
参照が同じ配列を指したままとなるため、Reactは変更を検知せず再レンダリングは発生しません。
7.2 完了済みのタスクを削除するボタンの追加
完了済みのタスクを一括削除する機能を実装します。第03回講義で学んだ「filter」を利用しています。
src/App.tsx に次の
removeCompletedTodos 関数を追加してください。
const removeCompletedTodos = () => {
const updatedTodos = todos.filter((todo) => !todo.isDone);
setTodos(updatedTodos);
};つづいて、removeCompletedTodos
関数を呼び出すためのボタンを追加してください。
<button
type="button"
onClick={removeCompletedTodos}
className={
"mt-5 rounded-md bg-red-500 px-3 py-1 font-bold text-white hover:bg-red-600"
}
>
完了済みのタスクを削除
</button>「完了済みのタスクを削除」のボタンを押下することで、実際に完了済みタスクの一括削除ができることを確認してください。
8 タスクの個別削除
次のように完了/未完了に関係なく タスクを個別に削除する機能 を実装します。
まずは src/App.tsx のなかに、次の
remove 関数を追加してください。これは引数で受け取った
id に該当する Todo を削除する関数になります。
const remove = (id: string) => {
const updatedTodos = todos.filter((todo) => todo.id !== id);
setTodos(updatedTodos);
};そして、この remove 関数を Props
として「TodoListコンポーネント」に渡すようにします。
次に、src/TodoList.tsx
で、Propsの型定義 を次のように変更します。
type Props = {
todos: Todo[];
updateIsDone: (id: string, value: boolean) => void;
remove: (id: string) => void; // ◀◀ 追加
};さらに、個々のTodoを表示するUIに「削除」のボタンを追加し、クリックされたときに、id
を引数にして remove
関数をコールするようにします。
<button
onClick={() => props.remove(todo.id)}
className="rounded-md bg-slate-200 px-2 py-1 text-sm font-bold text-white hover:bg-red-500"
>
削除
</button>9 リファクタリング: コンポーネントに分割
TodoListコンポーネントが肥大化してきたので、個々のTodoを「TodoItemコンポーネント
(src/TodoItem.tsx)」に切り分けます。Reactでは、コンポーネントに分割可能なものは、できるだけコンポーネントに分割するようにしてください。
実装例を以下に示します。
まずは、個々のTodo情報を表示するための
src/TodoItem.tsx
を新規作成します。ほとんどの部分は、既存の
src/TodoList.tsx
から移植して作成できると思います。
(以下のコードは、必要最低限の情報とUIだけを記述したものです)
import React from "react";
import { Todo } from "./types";
type Props = {
todo: Todo;
updateIsDone: (id: string, value: boolean) => void;
remove: (id: string) => void;
};
const TodoItem = (props: Props) => {
const todo = props.todo;
return (
<div className="flex items-center justify-between">
<div className="flex items-center">
<input
type="checkbox"
checked={todo.isDone}
onChange={(e) => props.updateIsDone(todo.id, e.target.checked)}
className="mr-1.5 cursor-pointer"
/>
{todo.name}
</div>
<div>
<button
onClick={() => props.remove(todo.id)}
className="rounded-md bg-slate-200 px-2 py-1 text-sm font-bold text-white hover:bg-red-500"
>
削除
</button>
</div>
</div>
);
};
export default TodoItem;上記で新規作成した「TodoItemコンポーネント」を、既存の「TodoListコンポーネント」から呼び出すようにします。特に 第03行目 と、第24行目 から 第31行目 に注目して読解してください。
import React from "react";
import { Todo } from "./types";
import TodoItem from "./TodoItem"; // ◀◀ 追加
type Props = {
todos: Todo[];
updateIsDone: (id: string, value: boolean) => void;
remove: (id: string) => void;
};
const TodoList = (props: Props) => {
const todos = props.todos;
if (todos.length === 0) {
return (
<div className="text-red-500">
現在、登録されているタスクはありません。
</div>
);
}
return (
<div className="space-y-2">
{todos.map((todo) => (
<TodoItem
key={todo.id}
todo={todo}
remove={props.remove}
updateIsDone={props.updateIsDone}
/>
))}
</div>
);
};
export default TodoList;10 LocalStorageを利用したデータの永続化
現状のTodoアプリは、Todoの追加や削除をしてもブラウザをリロード (再読み込み) すると初期状態に戻ってしまいます。このままでは、Todoアプリとして実用的に使えないため データの永続化 (Data Persistence) の機能が必要となります。「データの永続化」とは、簡単に言えば ブラウザをリロードしたとき や 次回アクセスしたとき にも継続的にアプリを利用できるように データや状態を不揮発性の領域に保存すること です。
ウェブアプリにおいて「データの永続化」は、通常、バックエンド側 (サーバ側) でデータベース (RDBやNoSQLなど) を利用して実装されます。バックエンドでの実装することで、ユーザは異なるデバイスからアクセスしても (ユーザ認証を経たうえで) 継続的にアプリの利用ができます。
しかし、まだ現時点ではバックエンド開発は学んでいないので、ここではブラウザの「LocalStorage」という機能を使って、フロントエンドでのデータの永続化
を実装します。LocalStorage とは、ウェブブラウザに組み込まれている
キーバリュー型のストレージ機能 (Pythonの
dict のようなシンプルな 「キー(key)」と「値(Value)」のペアでデータを保存する機能)
で、ブラウザを終了してもデータが残ることが特徴です。
10.1 LocalStorage とは
LocalStorageは、JavaScriptの Window.localStorage API を通じて利用可能で、保存可能なデータは 文字列 (string型) のみ ですが、約5MB までのデータが保存可能となっています。
LocalStorageは ブラウザごとに独立した保存領域 を持ちます。例えば、https://hoge777.github.io/react-todo-app/ にホストされた TodoApp に2人のユーザ「寝屋川タヌキ」と「萱島ウサギ」がアクセスした場合、各データは、各ユーザーが使用するデバイスのブラウザに独立して保存されます。そのため、例えば寝屋川タヌキが「高専に侵入」というTodoを追加しても、萱島ウサギは影響を受けません。
LocalStorage の容量制限
LocalStorage は、1オリジンあたり約5MB が一般的な容量制限になっています。ここでの「オリジン」とは「プロトコル(http/https)」「ドメイン」「ポート」の組み合わせを指します。
例えば、Todoアプリを GitHub Pagaes にデプロイするとすれば、次のようになります。
- https://hoge777.github.io/react-todo-app/ の LocalStorage は約5MB
- https://hoge777.github.io/portfolio/ は「同じオリジン」なので 上記とあわせて約5MB
- https://lofoo123.github.io/ → 異なるオリジンなので上記とは、別途、約5MB
また、ローカル環境の開発サーバでホストするとすれば、次のようになります。
- http://localhost:3000/ の LocalStorage は約5MB
- http://localhost:5500/ は異なるオリジン (異なるポート番号) なので、上記とは別途約5MB
10.1.1 定着確認
- LocalStorage
における1オリジンあたりの最大保存容量は約何MBか答えよ。
- 答え: 5MB
- 同じデバイスで Edge と Chrome
を使用しているとき、同一オリジンに対して各ブラウザは「独立したLocalStorageを持つ」か「共通のLocalStorageを持つ」か答えよ。
- 答え: 独立したLocalStorageを持つ
- LocalStorage
に画像などのバイナリデータを保存することは「可能」か「不可能」か答えよ。
- 答え: 不可能。LocalStorageは文字列(String型)のデータしか保存できない。ただし、Base64などで文字列にエンコードすれば可能である。
10.2 LocalStorage を利用した保存機能の実装
現在のTodoアプリに LocalStorage
を組み込んでデータを永続化するには src/App.tsx
に次のように実装します (useEffect という Reactの Hook
の利用が実装のポイントとなります)。
まずは、実際に動かしてみてください (リロードしてもTodoリストが初期状態に戻ってしまわないこと確認してください)。
import { useState, useEffect } from "react"; // ◀◀ 追加
// ~ 省略 ~
const App = () => {
const [todos, setTodos] = useState<Todo[]>([]); // ◀◀ 編集
const [newTodoName, setNewTodoName] = useState("");
const [newTodoPriority, setNewTodoPriority] = useState(3);
const [newTodoDeadline, setNewTodoDeadline] = useState<Date | null>(null);
const [newTodoNameError, setNewTodoNameError] = useState("");
const [initialized, setInitialized] = useState(false); // ◀◀ 追加
const localStorageKey = "TodoApp"; // ◀◀ 追加
// App コンポーネントの初回実行時のみLocalStorageからTodoデータを復元
useEffect(() => {
const todoJsonStr = localStorage.getItem(localStorageKey);
if (todoJsonStr && todoJsonStr !== "[]") {
const storedTodos: Todo[] = JSON.parse(todoJsonStr);
const convertedTodos = storedTodos.map((todo) => ({
...todo,
deadline: todo.deadline ? new Date(todo.deadline) : null,
}));
setTodos(convertedTodos);
} else {
// LocalStorage にデータがない場合は initTodos をセットする
setTodos(initTodos);
}
setInitialized(true);
}, []);
// 状態 todos または initialized に変更があったときTodoデータを保存
useEffect(() => {
if (initialized) {
localStorage.setItem(localStorageKey, JSON.stringify(todos));
}
}, [todos, initialized]);
const uncompletedCount = todos.filter(
(todo: Todo) => !todo.isDone
).length;
// ~ 以下、省略 ~ここでは useEffect が重要な役割を果たします。
まず、第15行目 ~ 第29行目 の
useEffect(() => {...}, []) によって
Appコンポーネントの 初回の実行時のみ
(ブラウザでリロードされたときは、そのリロードの実行時のみ)
LocalStorage から Todoデータ をロードして todos
にセットする仕組みをつくっています。
また、第32行目 ~ 第36行目 の
useEffect(() => {...}, [todos, initialized])
によって todos (または initialized)
に更新があるときに、LocalStorage に todos
をセーブする仕組みをつくっています。
じつは、ここまでの解説では、あえて触れていはいなかったのですが、「Reactによる画面更新のための再レンダリング」=第04回行目からの App関数 を再実行
という内部処理になっています。つまり、新規タスクの名前設定用の
テキストボックスに1文字入力するたびに
Reactにより画面の再レンダリングが実行されますが、その裏側では「App関数の再実行」が行なわれています
(このあたりの仕組みは後述の動画を参照してください)。そのため、useEffect
を利用しないと テキストボックスに1文字打ち込むたびにTodoデータのロードやセーブが実行
されてしまいます。ここでは、LocalStorage
を使っているため頻繁にロードとセーブをしても大きな問題はありません。しかし、一般的なウェブアプリでは
ウェブAPIを使ってデータの永続化
を行なうため、テキストボックスに1文字打ち込むたびにAPIを叩くような実装にすると、短期間でAPIの利用上限に到達したり、膨大な課金が発生してしまいます。注意してください。
useEffect の理解については 初心者向け!ReactのuseEffectが何してるのか20分で説明@
Code Tips (動画) やウェブ検索、生成AIを活用してください。
この他、永続化の処理では「LocalStorageには文字列しか保存できない」という制約に対応するために
JSON.parse() や JSON.stringify()
を利用しています。特に、Date型の値は JSON.stringify()
によって文字型に変換・保存された後、JSON.stringify()
にはDate型の値に復元されない点に注意してください
(文字列型として復元されます)。そのため 第21行目
のような工夫がされています。
以上のように、この永続化の処理は様々な工夫が詰められているので、色々と実験したり読み解いたりして理解を深めてください。
11 Reactアプリを GitHub Pages にデプロイする手順
Todoアプリを GitHub Pages にデプロイ するための手順について解説します。
以前に HTML/CSS でポートフォリオサイトを作成したときとは違って、単純に GitHub Pages の機能を有効化するだけでは「Reactアプリは機能しない」ため注意 してください。
11.1 準備
VSCodeのターミナルからgh-pagesというパッケージのインストールしてください。開発だけに使用するので
-D オプションを付けてください。
npm i -D gh-pages
また、同様に @types/node という型定義ファイルも
-D オプションを付けてインストールしてください。
npm i -D @types/node
プロジェクトフォルダを GitHub の パブリックリポジトリ として発行してください。既にリポジトリを発行済みの場合は、「コミット」と「同期 (Push)」を実行しておいてください。
11.2 初回設定
初回のみ必要な処理となります。
注意: 必ず mainブランチの コミットとプッシュ (同期) を完了させた状態 で、以下の処理を実行してください。mainブランチのなかに、未保存のファイルや、コミットしていないファイルが存在する状態で以下の処理を実行すると、予期せぬ問題が発生します。
VSCodeのターミナルから順番に実行してください。
git checkout --orphan gh-pages
git rm -rf .
git commit --allow-empty -m "Initial gh-pages commit"
git push origin gh-pages
git checkout main
以上は、GitHub Pages でウェブサイトをホストするための準備作業 として一般的に使用される一連のコマンドです。詳細について知りたいときは、生成AIを利用してください。
なお、git rm -rf .
によって一時的に全ファイルが消去されますが、
git checkout main で
mainブランチに戻ると復活するので安心してください。
11.3 GitHub Pages の設定
ウェブブラウザで「Todoアプリ」のリポジトリに移動して、以下のように設定されていることを確認してください。もし、自動的に設定されていない場合は、図のように設定してください。
なお、この時点では、https://xxxx.github.io/react-todo-app/ にアクセスしても 「404」 となります。実際に確認してください。
11.4 index.html の編集と 404.html の追加
プロジェクトフォルダの直下にある index.html の
<head> タグの子要素として、次のような
<script>
タグを追加してください。index.html の全体コードはこちらのようになります。
<script type="text/javascript">
var pathSegmentsToKeep = 1;
var l = window.location;
l.replace(
l.protocol +
"//" +
l.hostname +
(l.port ? ":" + l.port : "") +
l.pathname
.split("/")
.slice(0, 1 + pathSegmentsToKeep)
.join("/") +
"/?/" +
l.pathname
.slice(1)
.split("/")
.slice(pathSegmentsToKeep)
.join("/")
.replace(/&/g, "~and~") +
(l.search ? "&" + l.search.slice(1).replace(/&/g, "~and~") : "") +
l.hash
);
</script>また、同様に プロジェクトフォルダの直下 に
404.html というファイルを新規作成してこちらのコードを貼付けて保存してください。
11.5 各種設定ファイルの変更
package.json のなかに "homepage"
を追加し、さらに "scripts" のなかに
"predeploy" と "deploy"
を追加してください。
"homepage"のxxxxの部分は 自分のGitHubアカウント に書き換えてください。
"homepage": "https://xxxx.github.io/react-todo-app",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview",
"predeploy": "npm run build",
"deploy": "gh-pages -d dist"
},設定項目を追加する際は、カンマ ,
等の付け忘れに注意してください。また、JSONファイルに「コメント」を追加するとビルドエラーになる可能性があるので注意してください。
vite.config.ts
を以下のように編集してください。
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import path from "path"; // ◀◀ 追加
const repositoryName = "react-todo-app"; // ◀◀ 追加
export default defineConfig({
plugins: [react()],
// ▼▼ 追加 ここから ▼▼
base: process.env.NODE_ENV === "production" ? `/${repositoryName}/` : "/",
build: {
rollupOptions: {
input: {
main: path.resolve(__dirname, "index.html"),
404: path.resolve(__dirname, "404.html"),
},
},
},
// ▲▲ 追加 ここまで ▲▲
server: {
port: 3000,
strictPort: false,
open: true,
},
});一旦、ここまでの内容で「コミット」と「同期 (Push)」をしておいてください。以上で設定は終わりです。
11.6 発行 Publish
ここからは、Todoアプリを書き換えて、その内容を GitHub Pages に反映したいとき、その都度、実行する必要がある処理 になります。
VSCodeのターミナルから以下のコマンドを実行してください。
npm run predeploy
このコマンドは実質的に npm run build
と同じです。エラーがあれば修正してください。
特にエラーがなければ、以下のコマンドを実行してください。
npm run deploy
以下のように「Published」と出力されたらデプロイが
成功🎉 (完了)
です。以降、ソースファイルのコミットとプッシュにあわせて、適宜、上記の
npm run deploy でデプロイを実行してください。
デプロイ操作をしない限り、ソースコードをコミット&プッシュしても、GitHub
Pages にホストしているTodoアプリは更新されません。
PS C:\Users\xxxx\Documents\react-todo-app> npm run deploy
> react-todo-app@0.0.0 predeploy
> npm run build
> react-todo-app@0.0.0 build
> tsc -b && vite build
vite v5.4.9 building for production...
✓ 69 modules transformed.
dist/index.html 1.58 kB │ gzip: 0.77 kB
dist/404.html 1.92 kB │ gzip: 0.82 kB
dist/assets/main-wUGgSDZ4.css 8.60 kB │ gzip: 2.30 kB
dist/assets/main-DWqE2Lov.js 232.21 kB │ gzip: 76.33 kB
✓ built in 1.64s
> react-todo-app@0.0.0 deploy
> gh-pages -d dist
Published
ウェブブラウザから https://xxxx.github.io/react-todo-app/ にアクセスしてTodoアプリが利用できることを確認してください。
なお、react-router-domを使った ルーティング を導入したReactアプリを GitHub Pages でホストする場合は、さらにひと工夫が求められます。これについては個別に訊いてくれれば個別にレクチャします (main.tsx,Router.tsx)。
12 課題1
以上で、(フロントエンドだけで処理が完結するような) React を使った Todoアプリ の「チュートリアル」は完了です。
このチュートリアルで学んだことをベースとして オリジナルのTodoアプリ を開発して、GitHub Pages にデプロイし、その URL を提出してください。提出するURLは「リポジトリのURL (https://github.com/xxxx/react-todo-app)」ではなく、「Todoアプリをホストしている GitHub Pages のURL (https://xxxx.github.io/react-todo-app)」としてください。
- 提出期限 : 11月27日 (水) 23時
- 提出先 : Google Classroom
- 採点 :
「7点」を標準として「10点満点」で評価します。学年末の成績評価の際には「重み」が付けられます。
- 参考: Todoアプリのサンプル …ギリギリ合格水準のレベルの6点😨
- 参考: Todoアプリのサンプル …ここまでつくれたら素晴らしい!9点🎉
- 取り組み時間の目安 :
16時間以上 (
useEffectの理解などの基礎学習の時間を含む)。本科目は「学修単位科目」かつ「筆記試験がない科目」です。課題には十分な時間をかけて取り組んでください。 - アドバイス :
Todoアプリとしての「機能」や「UI/UX」を中心とした完成度の他、リポジトリの
README.mdの内容を評価します。README.mdに、以下の内容を含めると高評価です。- アプリの機能。これを書いておかないと頑張って実装した機能に気付いてもらえないことがあります。スクリーンショットがあると分かりやすいです。
- 特に工夫や注力 (チャレンジ) したところ、オリジナリティを持たせているところ。それらの「着想」や「経緯」についても書かれていると (就活のポートフォリオとして) 高評価・好印象です👍
- 使用技術 (技術スタック、主要ライブラリ)、開発に要した時間や期間など。
提出された課題 (URL) は、知能情報コースの学生 (知能情報コースを志望する1年生を含む)、本校教員に共有します。また、この課題は、インターンシップや就職の選考にポートフォリオとして使えるもの (=自分のスキルをや経験を証明するもの) なので、注力して取り組んで欲しいと考えています。