React + TypeScript 実践ベストプラクティス2026:プロが使うパターン集


2026年のReact + TypeScript開発の現状

React 19 + TypeScript 5.x の組み合わせは、2026年時点でフロントエンド開発の標準スタックとなっています。

採用企業のトレンド(2026年求人分析):

  • React必須:フロントエンド求人の約70%
  • TypeScript必須:フロントエンド求人の約65%
  • Next.js追加要件:フロントエンド求人の約50%

ベストプラクティス1:型定義の設計

避けるべきパターン

// ❌ 悪い例:anyを使う
function processData(data: any) {
  return data.map((item: any) => item.name);
}

// ❌ 悪い例:型アサーション乱用
const user = response.data as User;

推奨パターン

// ✅ 良い例:型推論を活用
interface ApiResponse<T> {
  data: T;
  status: 'success' | 'error';
  message?: string;
}

interface User {
  id: number;
  name: string;
  email: string;
  role: 'admin' | 'user' | 'viewer';
}

async function fetchUsers(): Promise<ApiResponse<User[]>> {
  const response = await fetch('/api/users');
  return response.json();
}

// ✅ 型ガードで安全に絞り込む
function isUser(value: unknown): value is User {
  return (
    typeof value === 'object' &&
    value !== null &&
    'id' in value &&
    'name' in value
  );
}

ベストプラクティス2:カスタムフック設計

// ✅ 汎用的なデータフェッチフック
interface FetchState<T> {
  data: T | null;
  loading: boolean;
  error: Error | null;
}

function useFetch<T>(url: string): FetchState<T> {
  const [state, setState] = useState<FetchState<T>>({
    data: null,
    loading: true,
    error: null,
  });

  useEffect(() => {
    let cancelled = false;

    const fetchData = async () => {
      try {
        const response = await fetch(url);
        if (!response.ok) throw new Error(`HTTP error: ${response.status}`);
        const data: T = await response.json();
        if (!cancelled) setState({ data, loading: false, error: null });
      } catch (error) {
        if (!cancelled) setState({
          data: null,
          loading: false,
          error: error instanceof Error ? error : new Error('Unknown error'),
        });
      }
    };

    fetchData();
    return () => { cancelled = true; };
  }, [url]);

  return state;
}

// 使用例
function UserList() {
  const { data: users, loading, error } = useFetch<User[]>('/api/users');

  if (loading) return <div>読み込み中...</div>;
  if (error) return <div>エラー: {error.message}</div>;
  if (!users) return null;

  return <ul>{users.map(user => <li key={user.id}>{user.name}</li>)}</ul>;
}

ベストプラクティス3:React Server Components(Next.js 15)

// ✅ Server Componentでデータフェッチ(パフォーマンス最適化)
// app/users/page.tsx
async function UsersPage() {
  // サーバーサイドで実行(バンドルサイズに影響しない)
  const users = await db.user.findMany({
    orderBy: { createdAt: 'desc' },
    take: 20,
  });

  return (
    <div>
      <h1>ユーザー一覧</h1>
      {users.map(user => (
        <UserCard key={user.id} user={user} />
      ))}
    </div>
  );
}

// ✅ 'use client'はインタラクティブな部分だけに
'use client';
function UserCard({ user }: { user: User }) {
  const [expanded, setExpanded] = useState(false);

  return (
    <div onClick={() => setExpanded(!expanded)}>
      <h2>{user.name}</h2>
      {expanded && <p>{user.email}</p>}
    </div>
  );
}

ベストプラクティス4:パフォーマンス最適化

// ✅ useMemoとuseCallbackの適切な使用
function ExpensiveList({ items, onSelect }: {
  items: Item[];
  onSelect: (id: number) => void;
}) {
  // 重い計算はuseMemoでメモ化
  const sortedItems = useMemo(
    () => [...items].sort((a, b) => a.priority - b.priority),
    [items] // itemsが変わった時だけ再計算
  );

  // コールバックはuseCallbackでメモ化(子コンポーネントへの影響を防ぐ)
  const handleSelect = useCallback(
    (id: number) => onSelect(id),
    [onSelect]
  );

  return (
    <ul>
      {sortedItems.map(item => (
        <MemoizedItem
          key={item.id}
          item={item}
          onSelect={handleSelect}
        />
      ))}
    </ul>
  );
}

// ✅ React.memoで不要な再レンダリングを防ぐ
const MemoizedItem = memo(function Item({
  item,
  onSelect,
}: {
  item: Item;
  onSelect: (id: number) => void;
}) {
  return <li onClick={() => onSelect(item.id)}>{item.name}</li>;
});

ベストプラクティス5:テスト戦略

// ✅ Vitestを使ったコンポーネントテスト
import { render, screen, fireEvent } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import { UserCard } from './UserCard';

describe('UserCard', () => {
  const mockUser: User = {
    id: 1,
    name: 'テスト太郎',
    email: 'test@example.com',
    role: 'user',
  };

  it('ユーザー名が表示される', () => {
    render(<UserCard user={mockUser} />);
    expect(screen.getByText('テスト太郎')).toBeInTheDocument();
  });

  it('クリックで詳細が展開される', () => {
    render(<UserCard user={mockUser} />);
    const card = screen.getByRole('button');
    fireEvent.click(card);
    expect(screen.getByText('test@example.com')).toBeInTheDocument();
  });
});

Vitestの詳しい使い方はVitest完全ガイド2026をご覧ください。


AIを活用したReact開発

2026年ではAIコーディングアシスタントとReact開発を組み合わせることが一般化しています。

# Claude Codeでコンポーネントを自動生成する例
claude

> "以下の要件でTypeScriptのReactコンポーネントを作ってください:
>  - ユーザー一覧をAPIから取得して表示する
>  - ローディング・エラー状態を適切に処理する
>  - テストコードも一緒に作ってください"

AIツールを効果的に活用する方法はClaude Code完全ガイド2026で詳しく解説しています。


ベストプラクティス6:カスタムフックの実践パターン

実際のプロジェクトでよく使う高度なカスタムフックのパターンを紹介します。

useLocalStorage — 型安全なローカルストレージ

useState のインターフェースでLocalStorageを操作するフックです。ジェネリクスで型を指定でき、SSR環境(typeof window === 'undefined')にも対応します。値の変更時に自動的にLocalStorageへ保存され、初期値はLocalStorageの既存値があればそちらを使います。


ベストプラクティス7:Error Boundaryの型安全な実装

React 19でもError Boundaryはクラスコンポーネントが必要ですが、TypeScriptで型安全に実装できます。

class ErrorBoundary extends React.Component<
  { children: React.ReactNode; fallback: React.ReactNode | ((error: Error) => React.ReactNode) },
  { hasError: boolean; error: Error | null }
> {
  state = { hasError: false, error: null as Error | null };

  static getDerivedStateFromError(error: Error) {
    return { hasError: true, error };
  }

  render() {
    if (this.state.hasError && this.state.error) {
      const { fallback } = this.props;
      return typeof fallback === 'function' ? fallback(this.state.error) : fallback;
    }
    return this.props.children;
  }
}

ベストプラクティス8:useTransitionによるUI応答性の改善

React 19の useTransition を使うと、重い状態更新をバックグラウンドで実行し、UIの応答性を維持できます。

function FilterableList({ items }: { items: Item[] }) {
  const [query, setQuery] = useState('');
  const [isPending, startTransition] = useTransition();
  const [filteredItems, setFilteredItems] = useState(items);

  const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
    setQuery(e.target.value); // 即座に入力欄を更新
    startTransition(() => {
      setFilteredItems(items.filter(item =>
        item.name.toLowerCase().includes(e.target.value.toLowerCase())
      ));
    });
  };

  return (
    <div>
      <input value={query} onChange={handleSearch} placeholder="検索..." />
      {isPending && <span>フィルタリング中...</span>}
      <ul>{filteredItems.map(item => <li key={item.id}>{item.name}</li>)}</ul>
    </div>
  );
}

関連記事