React.js入門:コンポーネント・props・stateを読書リストで学ぶ
React.jsのコンポーネント・props・stateを、検索と読了切り替え付きの読書リストで学びます。TypeScriptの完全なコード例、配列更新、key、useEffectの判断を解説。
Reactは、画面をコンポーネントに分け、データに応じて表示を組み立てるJavaScriptライブラリです。この記事ではTypeScriptで小さな読書リストを作り、コンポーネント、props、state、イベント処理の関係を学びます。検索、未読だけの表示、読了状態の切り替えまで実装します。
この記事で作るものと前提
読書リストの機能
- タイトルを入力して本を絞り込みます。
- 「未読だけ表示」で読了済みの本を隠します。
- 各行のチェックボックスで読了状態を切り替えます。
- 未読数と検索結果件数を表示します。
データはブラウザー上のメモリーに保持します。再読み込みすると初期状態に戻り、サーバーやストレージには保存しません。まず表示とデータ更新の基本に集中する構成です。
必要な知識と開発環境
JavaScriptの配列操作と関数、TypeScriptの基本的な型を知っていると読み進めやすくなります。JSXはJavaScriptの中にHTMLに似た記法で画面を書く仕組みです。TypeScriptでJSXを書くファイルには拡張子.tsxを使います。
独立したReactアプリを学ぶため、ここではViteのReact・TypeScriptテンプレートを使います。実際のサービスではルーティングやサーバー処理などの要件から、Next.jsなどのフレームワークも検討します。ViteそのものがReactのルーティング機能を提供するわけではありません。
プロジェクトを作成する
Viteのテンプレートを起動する
サポート中のNode.js LTSを用意し、利用するViteのNode.js要件を公式ガイドで確認してください。別の作業用ディレクトリで、次のコマンドを実行します。
npm create vite@latest react-reading-list -- --template react-ts
cd react-reading-list
npm install
npm run dev
ターミナルに表示されるローカルURLを開きます。テンプレートの依存バージョンは作成時点で変わるため、生成されたpackage.jsonとロックファイルをプロジェクトで管理してください。
編集するファイルを確認する
次の4ファイルを作成または置き換えます。テンプレートのsrc/main.tsxはそのまま利用し、src/index.cssの読み込みも残します。
src/
App.tsx
BookRow.tsx
ReadingList.tsx
index.css
コンポーネントとpropsで本の行を作る
BookRow.tsxで型と表示を定義する
src/BookRow.tsxを作成します。コンポーネント名は大文字で始めます。Bookは本のデータ構造、BookRowPropsは親から受け取る値の型です。
export type Book = {
id: string;
title: string;
read: boolean;
};
type BookRowProps = {
book: Book;
onToggle: (id: Book['id']) => void;
};
export function BookRow({ book, onToggle }: BookRowProps) {
return (
<li>
<label>
<input
type="checkbox"
checked={book.read}
onChange={() => onToggle(book.id)}
/>
<span>{book.title}:{book.read ? '読了' : '未読'}</span>
</label>
</li>
);
}
checkedが現在の読了状態を表示し、onChangeが操作を親に伝えます。labelでチェックボックスと説明を関連付けているので、文字をクリックしても操作できます。
propsを直接変更しない
propsは親から渡される入力です。子の中でbook.read = trueのように変更せず、onToggle(book.id)を呼んで更新を親に任せます。この構成では、読書リスト全体のデータを親が管理し、子は表示と操作の通知を担当します。
stateとイベントで読書リストを動かす
ReadingList.tsxで必要なstateだけを保持する
src/ReadingList.tsxを作成します。保存するstateは、本の配列、検索語、未読フィルターの3つです。検索結果と未読数は、この3つから計算できます。
import { useState } from 'react';
import { BookRow, type Book } from './BookRow';
const initialBooks: Book[] = [
{ id: 'react-basics', title: 'Reactの基本', read: false },
{ id: 'typescript-types', title: 'TypeScriptの型', read: true },
{ id: 'web-accessibility', title: 'Webアクセシビリティ', read: false },
];
export function ReadingList() {
const [books, setBooks] = useState<Book[]>(initialBooks);
const [query, setQuery] = useState('');
const [unreadOnly, setUnreadOnly] = useState(false);
const keyword = query.trim().toLocaleLowerCase('ja-JP');
const visibleBooks = books.filter((book) =>
book.title.toLocaleLowerCase('ja-JP').includes(keyword)
&& (!unreadOnly || !book.read)
);
const unreadCount = books.filter((book) => !book.read).length;
function toggleBook(id: Book['id']) {
setBooks((currentBooks) => currentBooks.map((book) =>
book.id === id ? { ...book, read: !book.read } : book
));
}
return (
<section aria-labelledby="reading-list-heading">
<h2 id="reading-list-heading">読みたい本</h2>
<label className="search-label">
タイトルで検索
<input
type="search"
value={query}
onChange={(event) => setQuery(event.target.value)}
placeholder="例:React"
/>
</label>
<label>
<input
type="checkbox"
checked={unreadOnly}
onChange={(event) => setUnreadOnly(event.target.checked)}
/>
未読だけ表示
</label>
<p role="status">
全{books.length}冊のうち未読{unreadCount}冊・表示{visibleBooks.length}冊
</p>
{visibleBooks.length > 0 ? (
<ul>
{visibleBooks.map((book) => (
<BookRow key={book.id} book={book} onToggle={toggleBook} />
))}
</ul>
) : (
<p>条件に合う本がありません。検索語やフィルターを変更してください。</p>
)}
</section>
);
}
配列を直接変更せず、新しい値を渡す
setBooksには更新関数を渡しています。Reactが渡す更新前の配列を基にmapで新しい配列を作り、対象の本だけをスプレッド構文でコピーして変更します。更新前の配列やオブジェクトを直接書き換えないので、更新関数を純粋に保てます。
stateの更新は、その場で現在の変数を書き換える操作ではありません。更新が反映された次のレンダーで、新しいbooksを使って画面が組み立てられます。前のstateから次のstateを作る処理には、ここで使った更新関数が適しています。
安定したkeyで行を識別する
key={book.id}は、Reactが兄弟要素の中で同じ本を追跡するための識別子です。絞り込みや並び替えがある一覧で配列の位置をkeyにすると、行の対応が変わって問題になることがあります。レンダーごとに乱数を作る方法も避け、本に紐づく安定したIDを使います。
keyはReact用の特別な値なので、子の通常のpropsには渡されません。この例では子が必要とするIDをbook.idから取得します。
アプリに組み込んで動作を確認する
App.tsxを置き換える
src/App.tsxを次の内容にします。テンプレートのApp.cssの読み込みは、このファイルに残さなくて構いません。
import { ReadingList } from './ReadingList';
export default function App() {
return (
<main>
<h1>小さな読書リスト</h1>
<p>気になる本を探して、読み終えたらチェックしましょう。</p>
<ReadingList />
</main>
);
}
index.cssで読みやすく整える
src/index.cssを置き換えます。キーボード操作時にフォーカス位置がわかるスタイルも指定します。
:root {
font-family: system-ui, sans-serif;
color: #172b35;
background: #f1f7fa;
line-height: 1.7;
}
* { box-sizing: border-box; }
body { margin: 0; }
main {
max-width: 48rem;
margin: 3rem auto;
padding: 1.5rem;
}
section {
background: white;
padding: 1.5rem;
border-radius: 1rem;
}
label { display: flex; align-items: center; gap: 0.75rem; }
.search-label { display: grid; margin-bottom: 1rem; }
input[type="search"] {
width: 100%;
min-width: 0;
padding: 0.75rem;
font: inherit;
border: 1px solid #607985;
border-radius: 0.5rem;
}
input[type="checkbox"] { width: 1.2rem; height: 1.2rem; flex-shrink: 0; }
input:focus-visible { outline: 3px solid #007f9c; outline-offset: 3px; }
ul { list-style: none; padding: 0; }
li { padding: 0.75rem 0; border-bottom: 1px solid #d7e2e8; }
操作とビルドを確認する
初期状態は全3冊、未読2冊、表示3冊です。「React」で検索すると1冊になり、未読フィルターを有効にしたままその本を読了にすると一覧から消えます。これは現在の条件に合わなくなったためで、本のデータが削除されたわけではありません。
- 検索語を消すと、フィルターに合う本が再表示されることを確認します。
- 存在しないタイトルで検索すると、0件の案内が出ることを確認します。
- Tabキーで入力欄とチェックボックスに移動し、Spaceキーで切り替えられることを確認します。
- ページを再読み込みすると初期状態へ戻ることを確認します。
開発サーバーとは別のターミナルで、ビルドも確認します。
npm run build
よくある疑問と設計のポイント
検索結果をstateに保存しないのはなぜですか
検索結果はbooks、query、unreadOnlyから計算できる値です。別のstateに保存すると、元のデータとの同期処理が増えます。まずはレンダー中に計算し、大量のデータで処理時間が問題になってから計測と最適化を検討します。
useEffectでフィルター処理をする必要はありますか
この実装には必要ありません。画面を描くために既存のデータを絞り込むだけなら、レンダー中の計算で足ります。Effectは外部システムとの同期などに使うもので、計算できる値を別のstateへ転記するために追加すると処理が複雑になります。
Next.jsでもそのまま使えますか
基本のReactの考え方は共通です。Next.jsのApp Routerでは、stateやイベント処理を使う入口のコンポーネントに'use client'が必要です。たとえばReadingList.tsxの先頭、importより前に指定します。Viteの今回の構成には必要ありません。Pages Routerにもこの指定は不要です。CSSの読み込み方などは、利用するルーターの構成に合わせてください。
データを保存するには何を追加しますか
ブラウザー内だけに残す場合はストレージ、複数端末で共有する場合はAPIやデータベースが候補です。保存先が決まると、読み込み状態、失敗時の表示、データ形式の検証も必要になります。今回のサンプルは、これらを含まないメモリー上の練習用アプリです。
次に学ぶことと参考資料
型とアプリ設計の理解を広げる
propsの型定義に慣れたら、TypeScriptの記事一覧で型の使い方を深めてみてください。ページ構成やWebアプリの開発に進む場合は、Next.jsの記事一覧も参考になります。
公式ドキュメント
コンポーネントに表示を分け、propsで値と操作を渡し、stateの更新から画面を組み立てる。この流れを動くコードで確かめると、機能を増やすときもデータの置き場所を判断しやすくなります。