はじめに
前回の記事ではTypeScriptの基本的な型注釈・インターフェース・ジェネリクスを解説して、これまでのJavaScriptコードに型安全性を持たせる方法を紹介しました。今回はその発展としてReactとViteを組み合わせてコンポーネントベースのUI開発を始める方法を解説します。Reactはこの連載で学んできたHTML・CSS・JavaScript・TypeScriptの知識をそのまま活かせるUIライブラリです。「難しそう」というイメージを持っている方も、基本的な考え方は今まで学んできたことの延長線上にあります。
1. Reactとは何か
1.1 ReactはUIを作るためのライブラリ
ReactはMeta(旧Facebook)が開発したJavaScriptのUIライブラリです。フレームワークではなく「ライブラリ」であるため、ルーティングや状態管理は別途ライブラリを追加することが多いです。
| 比較 | 内容 |
|---|---|
| React | UIを構築するためのコアライブラリ |
| Vue.js | テンプレート構文が中心のフレームワーク |
| Svelte | コンパイル時にVanilla JSに変換するフレームワーク |
| Next.js | ReactベースのフルスタックWebフレームワーク |
1.2 コンポーネントという考え方
Reactの中心概念はコンポーネントです。UIを「再利用可能な部品」に分割して組み合わせます。
従来のHTMLの考え方:
index.html → 1ファイルにすべてのHTMLが書かれている
Reactのコンポーネントの考え方:
App
├── Header(ヘッダーコンポーネント)
│ ├── Logo
│ └── Navigation
├── Main
│ ├── HeroSection
│ ├── PostList
│ │ ├── PostCard(繰り返し使われる)
│ │ ├── PostCard
│ │ └── PostCard
│ └── Sidebar
└── Footer
2. プロジェクトのセットアップ
2.1 React + Viteプロジェクトを作成する
bash
# Viteでプロジェクトを作成する
npm create vite@latest my-react-app
# テンプレートを選択するプロンプトが表示される
# → フレームワーク:React
# → バリアント:TypeScript(今回はTSを使う)
cd my-react-app
npm install
npm run dev
2.2 作成されたフォルダ構成
my-react-app/
├── index.html
├── package.json
├── tsconfig.json
├── vite.config.ts
├── public/
│ └── vite.svg
└── src/
├── main.tsx ← エントリーポイント
├── App.tsx ← ルートコンポーネント
├── App.css
├── index.css
└── assets/
└── react.svg
2.3 main.tsxを確認する
tsx
// src/main.tsx
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import './index.css';
import App from './App.tsx';
createRoot( document.getElementById( 'root' )! )
.render(
<StrictMode>
<App />
</StrictMode>
);
💡 .tsx という拡張子はTypeScript + JSXを使うファイルに使います: JSXを含むファイルは .tsx(TypeScript)または .jsx(JavaScript)とします。JSXを含まないファイルは .ts・.js を使います。
3. JSXの書き方
3.1 JSXとは
JSXはJavaScriptの中にHTMLのような記法を書ける構文拡張です。BabelやViteがJavaScriptに変換します。
// JSX(見た目はHTMLに似ているが実際はJavaScript)
const element = (
<div className="card">
<h2>タイトル</h2>
<p>本文テキスト</p>
</div>
);
// 上記は内部的にこう変換される
const element = React.createElement(
'div',
{ className: 'card' },
React.createElement( 'h2', null, 'タイトル' ),
React.createElement( 'p', null, '本文テキスト' )
);
3.2 HTMLとJSXの違い
// 主な違い
// ① classはclassNameと書く
<div className="container">...</div>
// ② for属性はhtmlForと書く
<label htmlFor="email">メールアドレス</label>
// ③ 自己終了タグは必ず閉じる
<input type="text" />
<br />
<img src="photo.jpg" alt="写真" />
// ④ JavaScriptの式は{}で埋め込む
const name = '山田太郎';
<p>こんにちは、{name}さん</p>
// ⑤ コメントは{/* */}で書く
{/* これはコメント */}
// ⑥ スタイルはオブジェクトで書く
<div style={{ color: 'red', fontSize: '16px' }}>テキスト</div>
// ⑦ 隣接する要素は1つのルート要素か<>で囲む必要がある
return (
<>
<h1>タイトル</h1>
<p>本文</p>
</>
);
4. 関数コンポーネントを作る
4.1 最もシンプルなコンポーネント
// src/components/Greeting.tsx
// 関数コンポーネント(現在の主流)
function Greeting() {
return (
<div className="greeting">
<h1>こんにちは!</h1>
<p>Reactへようこそ。</p>
</div>
);
}
export default Greeting;
4.2 propsで外から値を受け取る
// src/components/PostCard.tsx
// propsの型定義
interface PostCardProps {
title: string;
excerpt: string;
date: string;
category?: string; // オプショナル
imageUrl?: string;
}
// propsを受け取るコンポーネント
function PostCard( { title, excerpt, date, category, imageUrl }: PostCardProps ) {
return (
<article className="post-card">
{imageUrl && (
<div className="post-card__thumb">
<img src={imageUrl} alt={title} loading="lazy" />
</div>
)}
<div className="post-card__body">
<div className="post-card__meta">
<time dateTime={date}>{date}</time>
{category && (
<span className="post-card__category">{category}</span>
)}
</div>
<h2 className="post-card__title">{title}</h2>
<p className="post-card__excerpt">{excerpt}</p>
</div>
</article>
);
}
export default PostCard;
// 使い方(App.tsxなど)
import PostCard from './components/PostCard';
function App() {
return (
<PostCard
title="CSSのpositionプロパティを理解する"
excerpt="static・relative・absolute・fixed・stickyの違いを解説します。"
date="2025-01-15"
category="CSS"
/>
);
}
💡 imageUrl && (...) はJavaScriptの短絡評価を使った条件付きレンダリングです: imageUrl が undefined(falsy)なら何も表示せず、値がある(truthy)場合だけ <div> を表示します。
5. 繰り返しレンダリング(リスト表示)
5.1 配列をmapでコンポーネントに変換する
// src/components/PostList.tsx
import PostCard from './PostCard';
interface Post {
id: number;
title: string;
excerpt: string;
date: string;
category: string;
}
interface PostListProps {
posts: Post[];
}
function PostList( { posts }: PostListProps ) {
if ( posts.length === 0 ) {
return <p className="no-posts">投稿がありません。</p>;
}
return (
<div className="posts-grid">
{posts.map( post => (
// keyは必須(同じ階層で一意な値を使う)
<PostCard
key={post.id}
title={post.title}
excerpt={post.excerpt}
date={post.date}
category={post.category}
/>
) )}
</div>
);
}
export default PostList;
⚠️ key プロパティは必須です: Reactはリストを更新するとき key を使ってどの要素が変わったかを判断します。key がないとパフォーマンスの問題や予期しない表示バグが起きます。key にはインデックス(index)ではなく、安定したユニークなID(post.id など)を使いましょう。
6. Hooks(フック)の基本
6.1 useState:状態を管理する
// src/components/Counter.tsx
import { useState } from 'react';
function Counter() {
// [現在の値, 値を更新する関数] = useState(初期値)
const [count, setCount] = useState<number>( 0 );
return (
<div className="counter">
<p>カウント:{count}</p>
<button onClick={() => setCount( count + 1 )}>増やす</button>
<button onClick={() => setCount( count - 1 )}>減らす</button>
<button onClick={() => setCount( 0 )}>リセット</button>
</div>
);
}
6.2 useStateでオブジェクトを管理する
// フォームの状態管理
interface FormData {
name: string;
email: string;
message: string;
}
function ContactForm() {
const [formData, setFormData] = useState<FormData>( {
name: '',
email: '',
message: '',
} );
const [isSubmitting, setIsSubmitting] = useState( false );
const [successMessage, setSuccessMessage] = useState<string | null>( null );
// 汎用の入力ハンドラー
const handleChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
const { name, value } = e.target;
setFormData( prev => ({ ...prev, [name]: value }) );
// ↑ 前の状態を展開して該当フィールドだけ更新する
};
const handleSubmit = async ( e: React.FormEvent<HTMLFormElement> ) => {
e.preventDefault();
setIsSubmitting( true );
try {
// APIリクエスト(前回学んだfetch)
await fetch( '/api/contact', {
method: 'POST',
body: JSON.stringify( formData ),
headers: { 'Content-Type': 'application/json' },
} );
setSuccessMessage( 'お問い合わせを受け付けました。' );
setFormData( { name: '', email: '', message: '' } );
} catch {
alert( '送信に失敗しました。再度お試しください。' );
} finally {
setIsSubmitting( false );
}
};
return (
<form onSubmit={handleSubmit} className="contact-form">
{successMessage && (
<div className="form-message form-message--success" role="alert">
{successMessage}
</div>
)}
<div className="form-group">
<label htmlFor="name" className="form-label">お名前</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleChange}
className="form-input"
required
/>
</div>
<div className="form-group">
<label htmlFor="email" className="form-label">メールアドレス</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
className="form-input"
required
/>
</div>
<div className="form-group">
<label htmlFor="message" className="form-label">お問い合わせ内容</label>
<textarea
id="message"
name="message"
value={formData.message}
onChange={handleChange}
className="form-textarea"
rows={6}
required
/>
</div>
<button
type="submit"
className={`form-submit ${isSubmitting ? 'is-loading' : ''}`}
disabled={isSubmitting}
>
{isSubmitting ? '送信中...' : '送信する'}
</button>
</form>
);
}
6.3 useEffect:副作用を管理する
// src/components/PostList.tsx
import { useState, useEffect } from 'react';
import PostCard from './PostCard';
interface Post {
id: number;
title: string;
body: string;
}
function PostList() {
const [posts, setPosts] = useState<Post[]>( [] );
const [isLoading, setIsLoading] = useState( true );
const [error, setError] = useState<string | null>( null );
// コンポーネントがマウントされたときにAPIを呼ぶ
useEffect( () => {
const fetchPosts = async () => {
try {
const res = await fetch( 'https://jsonplaceholder.typicode.com/posts?_limit=6' );
const data = await res.json() as Post[];
setPosts( data );
} catch {
setError( '投稿の読み込みに失敗しました。' );
} finally {
setIsLoading( false );
}
};
fetchPosts();
}, [] ); // ← 空配列:マウント時に1度だけ実行する
if ( isLoading ) return <div className="loading">読み込み中...</div>;
if ( error ) return <div className="error" role="alert">{error}</div>;
return (
<div className="posts-grid">
{posts.map( post => (
<PostCard
key={post.id}
title={post.title}
excerpt={post.body}
date="2025-01-01"
/>
) )}
</div>
);
}
export default PostList;
💡 useEffect の第2引数の配列は「依存配列」です:
useEffect( () => { /* 処理 */ } ); // ❌ 毎レンダーで実行(無限ループの危険)
useEffect( () => { /* 処理 */ }, [] ); // ✅ マウント時に1度だけ実行
useEffect( () => { /* 処理 */ }, [id] ); // ✅ idが変わるたびに実行
7. カスタムフックで処理を共通化する
7.1 データ取得ロジックをカスタムフックに切り出す
// src/hooks/useFetch.ts
import { useState, useEffect } from 'react';
interface FetchState<T> {
data: T | null;
isLoading: boolean;
error: string | null;
}
// ジェネリクスを使って汎用的なカスタムフックを作る
function useFetch<T>( url: string ): FetchState<T> {
const [data, setData] = useState<T | null>( null );
const [isLoading, setIsLoading] = useState( true );
const [error, setError] = useState<string | null>( null );
useEffect( () => {
let isCancelled = false; // クリーンアップフラグ
const fetchData = async () => {
setIsLoading( true );
setError( null );
try {
const res = await fetch( url );
if ( !res.ok ) throw new Error( `HTTP Error: ${res.status}` );
const json = await res.json() as T;
if ( !isCancelled ) setData( json );
} catch ( err ) {
if ( !isCancelled ) {
setError( err instanceof Error ? err.message : '不明なエラー' );
}
} finally {
if ( !isCancelled ) setIsLoading( false );
}
};
fetchData();
// クリーンアップ関数:コンポーネントがアンマウントされたら実行
return () => {
isCancelled = true;
};
}, [url] ); // urlが変わるたびに再取得する
return { data, isLoading, error };
}
export default useFetch;
// 使い方(コンポーネント側がすっきりする)
import useFetch from '../hooks/useFetch';
function PostList() {
const { data: posts, isLoading, error } = useFetch<Post[]>(
'https://jsonplaceholder.typicode.com/posts?_limit=6'
);
if ( isLoading ) return <div>読み込み中...</div>;
if ( error ) return <div>{error}</div>;
if ( !posts ) return null;
return (
<div className="posts-grid">
{posts.map( post => (
<PostCard key={post.id} title={post.title} excerpt={post.body} date="" />
) )}
</div>
);
}
8. CSSのあて方
8.1 グローバルCSS
// src/main.tsx でimportする
import './index.css'; // グローバルCSS
8.2 CSSモジュール
/* src/components/PostCard.module.css */
.card {
background: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba( 0, 0, 0, 0.08 );
transition: transform 0.25s ease, box-shadow 0.25s ease;
}
.card:hover {
transform: translateY( -4px );
box-shadow: 0 8px 24px rgba( 0, 0, 0, 0.12 );
}
.title {
font-size: 1.8rem;
font-weight: 700;
line-height: 1.4;
}
// src/components/PostCard.tsx
import styles from './PostCard.module.css';
function PostCard( { title, excerpt }: PostCardProps ) {
return (
<article className={styles.card}>
<h2 className={styles.title}>{title}</h2>
<p>{excerpt}</p>
</article>
);
}
💡 CSSモジュールはクラス名の衝突を自動的に防ぎます: ビルド時にクラス名が PostCard_card__Abc12 のようにユニークな名前に変換されるため、他のコンポーネントのCSSと衝突しません。
9. コンポーネントの分割指針
9.1 どこでコンポーネントを分割するか
分割の目安:
① 同じUIを2箇所以上で使う → コンポーネント化する
② 1つのコンポーネントが100行を超えてきた → 分割を検討する
③ 異なる関心事が1コンポーネントに混在している → 分割する
④ 再利用はしないがロジックが複雑 → カスタムフックに切り出す
9.2 ディレクトリ構成の例
src/
├── components/ ← 再利用可能なUIコンポーネント
│ ├── ui/ ← 汎用的なUI部品
│ │ ├── Button.tsx
│ │ ├── Input.tsx
│ │ └── Card.tsx
│ └── features/ ← 機能ごとのコンポーネント
│ ├── PostCard.tsx
│ ├── PostList.tsx
│ └── ContactForm.tsx
├── hooks/ ← カスタムフック
│ ├── useFetch.ts
│ └── useLocalStorage.ts
├── types/ ← 型定義
│ ├── post.ts
│ └── user.ts
├── utils/ ← ユーティリティ関数
│ └── formatters.ts
├── App.tsx
└── main.tsx
10. 実践チェックリスト
✓ npm create vite@latest でReact + TypeScriptプロジェクトを作成したか
✓ JSXの className・htmlFor・自己終了タグなどHTMLとの違いを理解したか
✓ コンポーネントのpropsに interface で型を定義しているか
✓ useState で状態を管理して setState 以外の方法で状態を変更していないか
✓ リストを map() でレンダリングするとき key を設定しているか
✓ useEffect の依存配列を正しく設定しているか
✓ 繰り返し使うデータ取得ロジックをカスタムフックに切り出したか
✓ CSSモジュールまたはグローバルCSSでスタイルを適用しているか
✓ コンポーネントが大きくなりすぎていないか(100行を目安に分割を検討)
✓ npm run build でビルドエラーがないか確認したか
まとめ
今回はReact + ViteでコンポーネントベースのUI開発を始める方法を解説しました。ポイントをまとめると:
- ReactはUIをコンポーネントという再利用可能な部品に分割して管理するライブラリ
- JSXはJavaScriptの中にHTMLのような記法を書ける構文拡張で
className・htmlFor・自己終了タグなどHTMLとの違いがある - propsで親から子コンポーネントに値を渡して
interfaceで型を定義する useStateで状態を管理して値を変更するときは必ずsetState関数を使う- リストを
map()でレンダリングするときはkeyに安定したユニークIDを指定する useEffectはマウント時・依存値が変わったとき・アンマウント時の処理を管理する- データ取得のロジックはカスタムフックに切り出すと複数コンポーネントで再利用できる
- CSSモジュールを使うとクラス名の衝突を自動的に防げる
次の記事では、ReactのルーティングライブラリTanStack Router(または React Router)を使ってページ遷移を実装する方法を解説します。お楽しみに!
コメント