はじめに
この記事では、比較的シンプルながらWebアプリケーションの基本的な要素がたくさん詰まったToDoアプリを、Reactを使ってゼロから一緒に作っていきます。
なぜToDoアプリ?
- 基本が学べる: リスト表示、データの追加・変更・削除といった、多くのアプリで使われる基本操作を実装します。
この記事で作るToDoアプリの機能:
- 新しいToDoをテキスト入力して追加できる。
- 追加されたToDoがリスト表示される。
- ToDoをクリックすると完了/未完了状態を切り替えられる(見た目が変わる)。
- 不要なToDoを削除できる。
ステップ1: Reactプロジェクトの準備
まずは、Reactプロジェクトの土台を作ります。最近主流のViteというツールを使うと、簡単に高速な開発環境を準備できます。
ターミナル(WindowsならコマンドプロンプトやPowerShell、Macならターミナル)を開いて、以下のコマンドを実行してください。my-todo-app
は好きな名前に変えてOKです。
npm create vite@latest my-todo-app --template react-ts
いくつか質問されますが、基本的にはEnterキーを押していけばOKです。(もしNeed to install the following packages: create-vite@x.x.x Ok to proceed? (y)
と聞かれたら y
を入力してEnter)
次に、作成したプロジェクトのフォルダに移動し、必要なプログラム(ライブラリ)をインストールします。
cd my-todo-app
npm install
インストールが終わったら、開発用のサーバーを起動します。
npm run dev
ターミナルに Local: http://localhost:xxxx
のようなURLが表示されるので、そのURLをWebブラウザで開いてください。Reactのサンプルページが表示されれば成功です!
少しだけコードを整理しよう:
最初から入っているサンプルコードを少し整理して、開発を始めやすくしましょう。
-
src/App.css
: ファイルの中身をすべて削除して空にします。(後で自分でスタイルを書きます) -
src/index.css
: こちらも一旦中身をすべて削除してOKです。(必要なら後で基本的なスタイルを追加します) -
src/App.tsx
: このファイルがメインの画面を作る場所になります。中身を以下のようにシンプルに書き換えてください。// src/App.tsx import React from 'react'; // Reactを使うためのおまじない import './App.css'; // CSSファイルを読み込み function App() { // このreturn()の中に画面に表示したい内容を書いていく return ( <div className="App"> <h1>ToDoアプリ</h1> {/* ここにToDoアプリの要素を追加していく */} </div> ); } export default App; // 他のファイルで使えるようにするおまじない
ブラウザの表示が「ToDoアプリ」というタイトルだけのシンプルなものに変わればOKです。
ステップ2: UIの骨組みを作る (JSX)
まずは、ToDoアプリに必要な見た目の部品をHTMLのような形式で配置していきましょう。ReactではこれをJSXという記法で書きます。
src/App.tsx
を編集して、タイトル、ToDoを入力するフォーム、ToDoリストを表示するエリアの骨組みを作ります。
// src/App.tsx
import React from 'react';
import './App.css';
function App() {
return (
<div className="App">
<h1>ToDoアプリ</h1>
{/* ToDo入力フォーム */}
<form>
<input type="text" placeholder="新しいToDoを入力" />
<button type="submit">追加</button>
</form>
{/* ToDoリスト表示エリア */}
<h2>未完了のToDo</h2>
<ul>
{/* ここに未完了ToDoリストが表示される予定 */}
<li>サンプルToDo 1 (未完了)</li>
</ul>
<h2>完了したToDo</h2>
<ul>
{/* ここに完了ToDoリストが表示される予定 */}
<li>サンプルToDo 2 (完了)</li>
</ul>
</div>
);
}
export default App;
少しだけスタイルを整えよう (任意):
このままだと見た目が寂しいので、src/index.css
に少しだけCSSを追加して最低限の見た目を整えましょう。(CSSに慣れていない方は飛ばしてもOKです)
/* src/index.css */
body {
font-family: sans-serif;
margin: 20px;
background-color: #f4f4f4;
}
.App {
max-width: 600px;
margin: 0 auto;
background-color: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
h1, h2 {
text-align: center;
color: #333;
}
form {
display: flex;
margin-bottom: 20px;
}
input[type="text"] {
flex-grow: 1;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
margin-right: 10px;
}
button {
padding: 10px 15px;
background-color: #5cb85c;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #4cae4c;
}
ul {
list-style: none;
padding: 0;
}
li {
background-color: #eee;
margin-bottom: 10px;
padding: 10px;
border-radius: 4px;
display: flex;
justify-content: space-between;
align-items: center;
}
li button {
background-color: #d9534f;
padding: 5px 10px;
font-size: 0.9em;
}
li button:hover {
background-color: #c9302c;
}
/* 完了したToDo用のスタイル (後で使う) */
.completed {
text-decoration: line-through;
color: #777;
background-color: #e0e0e0;
}
ブラウザで確認し、入力フォームとサンプルのリストが表示されていればOKです。
ステップ3: ToDoリストを配列で表示する
今はサンプルToDoが直接書かれていますが、これをプログラムで扱えるように、JavaScriptの配列データからリスト表示するように変更します。
まず、App
関数の中(return
の前)に、サンプルのToDoデータを持つ配列を定義します。各ToDoは、内容を示す text
と、完了したかどうかを示す completed
、そして後で個々を区別するための id
を持つオブジェクトとします。
// src/App.tsx
import React from 'react';
import './App.css';
// ToDoアイテムの型を定義しておくと便利 (TypeScript)
type Todo = {
id: number;
text: string;
completed: boolean;
};
function App() {
// サンプルのToDoリスト配列
const initialTodos: Todo[] = [
{ id: 1, text: 'Reactの基本を学ぶ', completed: false },
{ id: 2, text: 'ToDoアプリを作る', completed: false },
{ id: 3, text: '休憩する', completed: true }, // 1つ完了済みにしてみる
];
// 配列の map メソッドを使って、各ToDoを<li>要素に変換する
const todoListItems = initialTodos.map(todo => (
// ★★★ key プロパティを必ず指定する ★★★
// Reactがリストの変更を効率的に追跡するために必要
<li key={todo.id}>
{todo.text}
</li>
));
return (
<div className="App">
<h1>ToDoアプリ</h1>
{/* ToDo入力フォーム (変更なし) */}
<form>
<input type="text" placeholder="新しいToDoを入力" />
<button type="submit">追加</button>
</form>
{/* ToDoリスト表示エリア */}
<h2>ToDoリスト</h2>
<ul>
{/* 配列から生成したリスト項目を表示 */}
{todoListItems}
</ul>
{/* 完了/未完了の表示分けは後でやるので一旦削除 */}
{/*
<h2>未完了のToDo</h2> ...
<h2>完了したToDo</h2> ...
*/}
</div>
);
}
export default App;
ポイント:
-
initialTodos.map(todo => ...)
: 配列のmap
メソッドは、配列の各要素に対して指定した処理を行い、その結果から新しい配列を作るJavaScriptの機能です。ここでは、各todo
オブジェクトを<li>
要素に変換しています。 -
key={todo.id}
:map
でリストを生成する場合、各要素にはユニークなkey
プロパティを指定する必要があります。これはReactがリスト項目を区別するために使います。ここでは各ToDoが持つid
を使っています。key
を指定しないとエラーが出たり、予期せぬ動作の原因になるので非常に重要です。
ブラウザで確認し、配列の内容がリスト表示されていれば成功です。
ステップ4: 新しいToDoを追加する機能 (Stateとフォーム)
いよいよアプリに動きをつけていきます!入力フォームにToDoを書いて「追加」ボタンを押したら、それがリストに加わるようにします。これにはReactのStateを使います。
Stateの準備
- 入力フォーム用のState: ユーザーが入力中のテキストを覚えておくためのState。
- ToDoリスト用のState: アプリ全体のToDoリストを保持するためのState。
useState
フックを import
し、App
関数の先頭でこれらを定義します。
// src/App.tsx
import React, { useState } from 'react'; // useState をインポート
import './App.css';
type Todo = {
id: number;
text: string;
completed: boolean;
};
function App() {
// 1. ToDoリスト全体を管理するState (初期値は空配列にする)
const [todos, setTodos] = useState<Todo[]>([]);
// 2. 入力フォームのテキストを管理するState (初期値は空文字列)
const [inputText, setInputText] = useState<string>('');
// --- ここから下はまだ変更しない ---
// const initialTodos = [...]; // これはもう使わないのでコメントアウトか削除
// const todoListItems = initialTodos.map(...); // これも後で変える
// ... return の中身 ...
return (
// ...
);
}
export default App;
フォーム入力とStateの連携 (制御コンポーネント)
入力フォームの <input>
要素が変更されるたびに、inputText
Stateを更新するようにします。
- inputの値が変わったときに呼ばれる関数
handleInputChange
を定義します。 -
<input>
要素にvalue
属性とonChange
属性を追加します。
// src/App.tsx
import React, { useState } from 'react';
import './App.css';
type Todo = { id: number; text: string; completed: boolean; };
function App() {
const [todos, setTodos] = useState<Todo[]>([]);
const [inputText, setInputText] = useState<string>('');
// inputの値が変わったらinputText Stateを更新する関数
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setInputText(event.target.value); // 入力された値でStateを更新
};
// ToDoリストの表示(後で修正)
const todoListItems = todos.map(todo => ( // initialTodos を todos に変更
<li key={todo.id}>
{todo.text}
</li>
));
return (
<div className="App">
<h1>ToDoアプリ</h1>
{/* ToDo入力フォーム */}
<form> {/* onSubmitは後で追加 */}
<input
type="text"
placeholder="新しいToDoを入力"
value={inputText} // inputの値をStateと紐付け
onChange={handleInputChange} // 値が変わったら関数を呼ぶ
/>
<button type="submit">追加</button>
</form>
{/* ToDoリスト表示エリア */}
<h2>ToDoリスト</h2>
<ul>
{todoListItems} {/* Stateからリストを表示 */}
</ul>
</div>
);
}
export default App;
これで、入力フォームに文字を打つと inputText
Stateがリアルタイムに更新されるようになりました(見た目上の変化はまだありません)。これを制御コンポーネントと言います。
ToDoリストへの追加処理
次に、フォームが送信(「追加」ボタンがクリック)されたときの処理を作ります。
- フォーム送信時に呼ばれる関数
handleAddTodo
を定義します。 -
<form>
要素にonSubmit
属性を追加します。
// src/App.tsx
import React, { useState } from 'react';
import './App.css';
type Todo = { id: number; text: string; completed: boolean; };
function App() {
const [todos, setTodos] = useState<Todo[]>([]);
const [inputText, setInputText] = useState<string>('');
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setInputText(event.target.value);
};
// フォームが送信されたときの処理
const handleAddTodo = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault(); // フォーム送信時のページの再読み込みを防ぐおまじない
if (inputText.trim() === '') return; // 入力が空なら何もしない
// 新しいToDoオブジェクトを作成
const newTodo: Todo = {
id: Date.now(), // ユニークIDとして現在時刻を使う(簡易的)
text: inputText,
completed: false, // 最初は未完了
};
// ★ Stateを更新: 既存のtodos配列の末尾に新しいToDoを追加した「新しい配列」を作る
setTodos([...todos, newTodo]);
// `...todos` は配列を展開するJavaScriptの書き方
setInputText(''); // 追加後、入力フォームを空にする
};
// ToDoリストの表示
const todoListItems = todos.map(todo => (
<li key={todo.id}>
{todo.text}
</li>
));
return (
<div className="App">
<h1>ToDoアプリ</h1>
{/* ToDo入力フォーム */}
<form onSubmit={handleAddTodo}> {/* 送信時に関数を呼ぶ */}
<input
type="text"
placeholder="新しいToDoを入力"
value={inputText}
onChange={handleInputChange}
/>
<button type="submit">追加</button>
</form>
{/* ToDoリスト表示エリア */}
<h2>ToDoリスト</h2>
<ul>
{todoListItems}
</ul>
</div>
);
}
export default App;
重要なポイント:
-
event.preventDefault()
: formのデフォルトの送信動作(ページリロード)をキャンセルします。ReactのようなSPA(Single Page Application)では必須です。 -
setTodos([...todos, newTodo])
: Stateを直接変更してはいけません! (todos.push(newTodo)
はNG)。必ずsetTodos
のような更新関数に、新しい配列を渡す必要があります。[...todos, newTodo]
は、元のtodos
配列の全要素を展開し、末尾にnewTodo
を追加した新しい配列を作るためのJavaScriptの便利な書き方(スプレッド構文)です。
これで、フォームにToDoを入力して「追加」ボタンを押すと、リストに項目が増えるはずです!試してみてください。
ステップ5: ToDoの完了/未完了を切り替える機能
次に追加したToDoをクリックしたら、打ち消し線が付く(完了状態になる)ようにします。もう一度クリックしたら元に戻る(未完了状態)ようにします。
- ToDoアイテムをクリックしたときに呼ばれる関数
handleToggleComplete
を定義します。どのToDoがクリックされたか区別するためにid
を引数で受け取ります。 - リスト項目
<li>
にonClick
イベントハンドラを追加します。 -
<li>
の見た目をcompleted
状態に応じて変えます。
// src/App.tsx
import React, { useState } from 'react';
import './App.css';
type Todo = { id: number; text: string; completed: boolean; };
function App() {
const [todos, setTodos] = useState<Todo[]>([]);
const [inputText, setInputText] = useState<string>('');
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setInputText(event.target.value);
};
const handleAddTodo = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (inputText.trim() === '') return;
const newTodo: Todo = { id: Date.now(), text: inputText, completed: false };
setTodos([...todos, newTodo]);
setInputText('');
};
// ToDoの完了/未完了を切り替える関数
const handleToggleComplete = (id: number) => {
setTodos(
// todos配列をmapで処理して新しい配列を作る
todos.map(todo =>
// もし現在のtodoのidが、クリックされたidと同じなら
todo.id === id
// completedプロパティを反転させた新しいオブジェクトを返す
? { ...todo, completed: !todo.completed }
// idが違う場合は、元のtodoオブジェクトをそのまま返す
: todo
)
);
};
// ToDoリストの表示部分を修正
const todoListItems = todos.map(todo => (
<li
key={todo.id}
// classNameをcompleted状態に応じて変える
className={todo.completed ? 'completed' : ''}
// クリックしたらhandleToggleCompleteを呼ぶ (idを渡す)
onClick={() => handleToggleComplete(todo.id)}
style={{ cursor: 'pointer' }} // クリックできることがわかるようにカーソルを変える
>
{todo.text}
{/* 削除ボタンは次のステップで追加 */}
</li>
));
return (
<div className="App">
<h1>ToDoアプリ</h1>
<form onSubmit={handleAddTodo}>
<input
type="text"
placeholder="新しいToDoを入力"
value={inputText}
onChange={handleInputChange}
/>
<button type="submit">追加</button>
</form>
<h2>ToDoリスト</h2>
<ul>
{todoListItems}
</ul>
</div>
);
}
export default App;
ポイント:
-
handleToggleComplete
: ここでもsetTodos
を使って新しい配列を作っています。map
を使って、該当するid
のToDoだけcompleted
の値を反転 (!todo.completed
) させた新しいオブジェクト ({ ...todo, completed: ... }
) に置き換え、それ以外のToDoは元のままにして、新しい配列を生成しています。 -
className={todo.completed ? 'completed' : ''}
: 条件(三項)演算子を使って、todo.completed
がtrue
ならcompleted
というCSSクラス名を、false
なら空文字列をclassName
に設定しています。src/index.css
に.completed
スタイルを定義したので、これで打ち消し線が付くようになります。 -
onClick={() => handleToggleComplete(todo.id)}
:onClick
に直接関数を渡すのではなく、アロー関数() => ...
の中でhandleToggleComplete(todo.id)
を呼び出しています。こうしないと、レンダリング時にhandleToggleComplete
が意図せず実行されてしまうためです。また、これによりクリックされたToDoのid
を関数に渡すことができます。
リストの項目をクリックして、打ち消し線が付いたり消えたりするか試してみてください。
ステップ6: ToDoを削除する機能
最後に、不要になったToDoを削除する機能を追加しましょう。
- 削除ボタンを各ToDoアイテムに追加します。
- 削除ボタンがクリックされたときに呼ばれる関数
handleDeleteTodo
を定義します。これもid
を引数で受け取ります。 -
<li>
内の削除ボタンにonClick
イベントハンドラを追加します。
// src/App.tsx
import React, { useState } from 'react';
import './App.css';
type Todo = { id: number; text: string; completed: boolean; };
function App() {
const [todos, setTodos] = useState<Todo[]>([]);
const [inputText, setInputText] = useState<string>('');
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => { setInputText(event.target.value); };
const handleAddTodo = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (inputText.trim() === '') return;
const newTodo: Todo = { id: Date.now(), text: inputText, completed: false };
setTodos([...todos, newTodo]);
setInputText('');
};
const handleToggleComplete = (id: number) => { setTodos(todos.map(todo => todo.id === id ? { ...todo, completed: !todo.completed } : todo)); };
// ToDoを削除する関数
const handleDeleteTodo = (id: number) => {
// 確認ダイアログを出す(任意)
// if (!window.confirm('本当に削除しますか?')) {
// return;
// }
setTodos(
// todos配列をfilterで処理して新しい配列を作る
// クリックされたidと「異なる」idを持つToDoだけを残す
todos.filter(todo => todo.id !== id)
);
};
// ToDoリストの表示部分を修正
const todoListItems = todos.map(todo => (
<li
key={todo.id}
className={todo.completed ? 'completed' : ''}
>
{/* ToDoテキスト部分をクリックでトグルするように変更 */}
<span onClick={() => handleToggleComplete(todo.id)} style={{ cursor: 'pointer', flexGrow: 1 }}>
{todo.text}
</span>
{/* 削除ボタンを追加 */}
<button onClick={() => handleDeleteTodo(todo.id)}>削除</button>
</li>
));
return (
<div className="App">
<h1>ToDoアプリ</h1>
<form onSubmit={handleAddTodo}>
<input
type="text"
placeholder="新しいToDoを入力"
value={inputText}
onChange={handleInputChange}
/>
<button type="submit">追加</button>
</form>
<h2>ToDoリスト</h2>
<ul>
{todoListItems}
</ul>
</div>
);
}
export default App;
ポイント:
-
handleDeleteTodo
: JavaScriptの配列メソッドfilter
を使っています。filter
は、指定した条件に合う要素だけを集めて新しい配列を作ります。ここではtodo.id !== id
という条件、つまり「クリックされた削除ボタンのToDoのidではない」ToDoだけを残しています。これにより、指定したidのToDoが除外された新しい配列が作られ、setTodos
でStateが更新されます。 - 削除ボタンの
onClick
でも、トグルと同様にアロー関数を使ってhandleDeleteTodo(todo.id)
を呼び出し、id
を渡しています。 - ToDoテキストを
<span>
で囲み、onClick
をそちらに移動しました。こうすることで、テキスト部分だけがトグル操作、削除ボタンは削除操作、と役割が明確になります。flexGrow: 1
を<span>
に追加すると、テキストが可能な限り幅を取り、ボタンが右端に配置されやすくなります(CSSの調整)。
これでToDoの削除機能も実装できました!
ステップ7: コンポーネント分割
現状、すべての機能が App.tsx
という一つのファイルに書かれています。このくらいの規模なら問題ありませんが、アプリがもっと大きくなると、一つのファイルに何百行もコードがあると、読みにくく、修正も大変になります。
そこで重要になるのがコンポーネント分割です。UIの部品ごとにファイルを分け、それぞれを独立したコンポーネントとして作ります。
今回のToDoアプリなら、例えば以下のように分割できます。
-
App.tsx
(親コンポーネント): 全体のレイアウト、ToDoリスト(todos
)と入力テキスト(inputText
)のState管理、各種イベントハンドラ関数(handleAddTodo
,handleToggleComplete
,handleDeleteTodo
)を持つ。 -
TodoForm.tsx
(子コンポーネント): ToDo入力フォームの見た目を担当。親からinputText
,handleInputChange
,handleAddTodo
をPropsとして受け取る。 -
TodoList.tsx
(子コンポーネント): ToDoリスト全体の表示エリア (<ul>
) を担当。親からtodos
配列、handleToggleComplete
,handleDeleteTodo
をPropsとして受け取る。 -
TodoItem.tsx
(孫コンポーネント): 個々のToDoアイテム (<li>
) の表示と操作を担当。TodoList
から個々のtodo
オブジェクト、handleToggleComplete
,handleDeleteTodo
をPropsとして受け取る。
なぜ分割するの?
- 可読性向上: 各ファイルが特定の役割に集中するので、コードが読みやすくなります。
-
再利用性向上: 作成したコンポーネント(例:
TodoForm
)は、別の場所でも再利用できる可能性があります。 - 保守性向上: 特定の機能を修正したい場合、関連するコンポーネントファイルだけを見れば良くなるため、影響範囲がわかりやすく、修正が容易になります。
コンポーネント分割は、React開発において非常に重要なスキルです。最初は難しく感じるかもしれませんが、「この部分は部品として切り出せそうだな」と考えながら開発を進める癖をつけると良いでしょう。(具体的な分割コードは長くなるためここでは割愛しますが、ぜひ挑戦してみてください!)
まとめ
入力、表示、更新、削除というCRUD (Create, Read, Update, Delete) と呼ばれる基本的なデータ操作を、ReactのStateとイベント処理を使って実装する流れを体験できたかと思います。
Reactには、今回学んだこと以外にも useEffect
フックによる副作用の管理、Context API
を使った効率的なデータ共有、React Router を使った複数ページ間の画面遷移など、さらに多くの機能や概念があります。
参考サイト:
- React 公式サイト: https://react.dev/
- React Router (ルーティング): https://reactrouter.com/
!