なぜReact + TypeScriptが現代のフロントエンド標準なのか
2026年現在、フロントエンド開発の世界標準はReact + TypeScriptの組み合わせになっています。Reactは世界中の5,000万以上のWebサイトで使われており、TypeScriptを加えることで型安全性と開発効率が大幅に向上します。本記事では、TypeScript初心者〜中級者がReactをより深く使いこなすための実践テクニックを紹介します。
TypeScriptの型システムをReactで最大活用する
1. コンポーネントのProps型定義
import React, { FC, ReactNode } from 'react';
// interfaceでPropsを定義
interface CardProps {
title: string;
description: string;
imageUrl?: string; // オプショナル
onClick: () => void;
children?: ReactNode;
variant: 'primary' | 'secondary' | 'danger'; // ユニオン型
}
// FC(FunctionComponent)で型付き関数コンポーネントを定義
const Card: FC = ({
title,
description,
imageUrl,
onClick,
children,
variant = 'primary'
}) => {
return (
<div className={`card card--${variant}`} onClick={onClick}>
{imageUrl && <img src={imageUrl} alt={title} loading="lazy" />}
<h2>{title}</h2>
<p>{description}</p>
{children}
</div>
);
};
export default Card;
2. Generic型を使った再利用可能なコンポーネント
// ジェネリクスを使った汎用リストコンポーネント
interface ListProps<T> {
items: T[];
renderItem: (item: T, index: number) => React.ReactNode;
keyExtractor: (item: T) => string;
emptyMessage?: string;
}
function List<T>({ items, renderItem, keyExtractor, emptyMessage = "データがありません" }: ListProps<T>) {
if (items.length === 0) {
return <p className="empty-message">{emptyMessage}</p>;
}
return (
<ul>
{items.map((item, index) => (
<li key={keyExtractor(item)}>
{renderItem(item, index)}
</li>
))}
</ul>
);
}
// 使用例
interface Article {
id: number;
title: string;
author: string;
}
const ArticleList = () => {
const articles: Article[] = [
{ id: 1, title: "FastAPI入門", author: "kasata" },
{ id: 2, title: "React TypeScript", author: "kasata" }
];
return (
<List
items={articles}
keyExtractor={(item) => item.id.toString()}
renderItem={(article) => (
<span>{article.title} - {article.author}</span>
)}
/>
);
};
カスタムフック(Custom Hooks)で状態ロジックを再利用する
カスタムフックはReactの最も強力な機能の一つです。状態管理ロジックをコンポーネントから分離することで、再利用性とテスト容易性が大幅に向上します。
// useApi:API呼び出しを抽象化するカスタムフック
import { useState, useEffect, useCallback } from 'react';
interface UseApiResult<T> {
data: T | null;
loading: boolean;
error: Error | null;
refetch: () => void;
}
function useApi<T>(url: string): UseApiResult<T> {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
const fetchData = useCallback(async () => {
try {
setLoading(true);
setError(null);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result: T = await response.json();
setData(result);
} catch (err) {
setError(err instanceof Error ? err : new Error('Unknown error'));
} finally {
setLoading(false);
}
}, [url]);
useEffect(() => {
fetchData();
}, [fetchData]);
return { data, loading, error, refetch: fetchData };
}
// 使用例
const ArticleDetail = ({ id }: { id: number }) => {
const { data, loading, error, refetch } = useApi<Article>(
`/api/articles/${id}`
);
if (loading) return <div className="spinner">読み込み中...</div>;
if (error) return <button onClick={refetch}>再試行</button>;
if (!data) return null;
return <h1>{data.title}</h1>;
};
状態管理:useReducerとuseContextで複雑な状態を管理
import React, { createContext, useContext, useReducer, ReactNode } from 'react';
// 型定義
interface CartItem {
id: number;
name: string;
price: number;
quantity: number;
}
interface CartState {
items: CartItem[];
total: number;
}
type CartAction =
| { type: 'ADD_ITEM'; payload: CartItem }
| { type: 'REMOVE_ITEM'; payload: { id: number } }
| { type: 'CLEAR_CART' };
// Reducerの実装
const cartReducer = (state: CartState, action: CartAction): CartState => {
switch (action.type) {
case 'ADD_ITEM': {
const existingItem = state.items.find(item => item.id === action.payload.id);
const updatedItems = existingItem
? state.items.map(item =>
item.id === action.payload.id
? { ...item, quantity: item.quantity + 1 }
: item
)
: [...state.items, action.payload];
const total = updatedItems.reduce((sum, item) => sum + item.price * item.quantity, 0);
return { items: updatedItems, total };
}
case 'REMOVE_ITEM': {
const items = state.items.filter(item => item.id !== action.payload.id);
const total = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
return { items, total };
}
case 'CLEAR_CART':
return { items: [], total: 0 };
default:
return state;
}
};
// Context の作成
const CartContext = createContext<{
state: CartState;
dispatch: React.Dispatch<CartAction>;
} | undefined>(undefined);
// Provider コンポーネント
export const CartProvider = ({ children }: { children: ReactNode }) => {
const [state, dispatch] = useReducer(cartReducer, { items: [], total: 0 });
return (
<CartContext.Provider value={{ state, dispatch }}>
{children}
</CartContext.Provider>
);
};
// カスタムフック
export const useCart = () => {
const context = useContext(CartContext);
if (!context) throw new Error('useCart must be used within CartProvider');
return context;
};
パフォーマンス最適化:memo・useMemo・useCallback
import React, { memo, useMemo, useCallback, useState } from 'react';
// memo:propsが変わらない場合に再レンダリングをスキップ
const ExpensiveChild = memo(({ data, onSelect }: {
data: string[];
onSelect: (item: string) => void;
}) => {
console.log('ExpensiveChildがレンダリングされました');
return (
<ul>
{data.map(item => (
<li key={item} onClick={() => onSelect(item)}>{item}</li>
))}
</ul>
);
});
const ParentComponent = () => {
const [count, setCount] = useState(0);
const [selected, setSelected] = useState('');
// useMemo:重い計算をメモ化(countが変わった時だけ再計算)
const processedData = useMemo(() => {
console.log('データを処理中...');
return ['Python', 'TypeScript', 'Go', 'Rust'].filter(lang =>
lang.length > count
);
}, [count]);
// useCallback:関数をメモ化(子コンポーネントへの不要な再レンダリングを防ぐ)
const handleSelect = useCallback((item: string) => {
setSelected(item);
}, []); // 依存関係なし = 一度だけ作成
return (
<div>
<p>カウント: {count} | 選択: {selected}</p>
<button onClick={() => setCount(c => c + 1)}>+1</button>
<ExpensiveChild data={processedData} onSelect={handleSelect} />
</div>
);
};
Next.jsとの組み合わせでSEO対策
ReactのみだとCSR(クライアントサイドレンダリング)になりSEOに不利ですが、Next.jsを使うことでSSR/SSGが可能になります。
// Next.js 14 App Router での Server Component活用
// app/articles/[id]/page.tsx
import { Metadata } from 'next';
interface Article {
id: number;
title: string;
content: string;
author: string;
}
// 動的メタデータ(SEO最適化)
export async function generateMetadata({ params }: { params: { id: string } }): Promise<Metadata> {
const article = await fetch(`https://api.example.com/articles/${params.id}`).then(r => r.json());
return {
title: `${article.title} | Tech Athletes`,
description: article.content.slice(0, 150),
openGraph: {
title: article.title,
description: article.content.slice(0, 150),
type: 'article',
}
};
}
// Server Componentでデータ取得(SEOフレンドリー)
export default async function ArticlePage({ params }: { params: { id: string } }) {
const article: Article = await fetch(
`https://api.example.com/articles/${params.id}`,
{ next: { revalidate: 3600 } } // 1時間ごとにISR
).then(r => r.json());
return (
<article>
<h1>{article.title}</h1>
<p>著者: {article.author}</p>
<div dangerouslySetInnerHTML={{ __html: article.content }} />
</article>
);
}
まとめ:React + TypeScript習得のロードマップ
React + TypeScriptをマスターするためのステップをまとめます:
- TypeScript基礎:型定義、ジェネリクス、ユニオン型をマスター
- Reactフック:useState、useEffect、useContextを完全理解
- カスタムフック:ロジックの再利用パターンを習得
- 状態管理:Context + useReducer、またはZustand/Jotaiを導入
- パフォーマンス最適化:memo、useMemo、useCallbackを使いこなす
- Next.js移行:SSR/SSG/ISRでSEO対策と高速化を実現
TypeScriptを使いこなすことで、バグの早期発見・チーム開発の効率化・リファクタリングの安全性が格段に向上します。ぜひ今日から導入を検討してみてください。