Welcome to Tech Athletes | テック・アスリート   Click to listen highlighted text! Welcome to Tech Athletes | テック・アスリート

【2026年版】React完全ガイド|Hooks・状態管理・パフォーマンス最適化・テストまで徹底解説

Reactとは?2026年も最前線で使われる理由

React(リアクト)はMeta(旧Facebook)が開発したJavaScriptライブラリで、2026年現在もフロントエンド開発の主流として広く使われています。コンポーネントベースのアーキテクチャ仮想DOMによるパフォーマンスの高さが特徴です。

React環境構築

Viteを使った高速セットアップ

# Viteを使ってReactプロジェクトを作成(2026年推奨)
npm create vite@latest my-react-app -- --template react-ts

cd my-react-app
npm install
npm run dev

React Hooksの完全マスター

useState – 状態管理の基本

import React, { useState } from 'react';

interface Task {
  id: number;
  title: string;
  completed: boolean;
}

const TodoApp: React.FC = () => {
  const [tasks, setTasks] = useState([]);
  const [inputValue, setInputValue] = useState('');
  const [filter, setFilter] = useState<'all' | 'active' | 'completed'>('all');

  const addTask = () => {
    if (!inputValue.trim()) return;
    
    const newTask: Task = {
      id: Date.now(),
      title: inputValue,
      completed: false,
    };
    setTasks(prev => [...prev, newTask]);
    setInputValue('');
  };

  const toggleTask = (id: number) => {
    setTasks(prev =>
      prev.map(task =>
        task.id === id ? { ...task, completed: !task.completed } : task
      )
    );
  };

  const filteredTasks = tasks.filter(task => {
    if (filter === 'active') return !task.completed;
    if (filter === 'completed') return task.completed;
    return true;
  });

  return (
    <div className="todo-app">
      <h1>タスク管理アプリ</h1>
      
      <div className="input-area">
        <input
          type="text"
          value={inputValue}
          onChange={(e) => setInputValue(e.target.value)}
          onKeyPress={(e) => e.key === 'Enter' && addTask()}
          placeholder="タスクを入力..."
        />
        <button onClick={addTask}>追加</button>
      </div>
      
      <div className="filters">
        {(['all', 'active', 'completed'] as const).map(f => (
          <button
            key={f}
            className={filter === f ? 'active' : ''}
            onClick={() => setFilter(f)}
          >
            {f === 'all' ? '全て' : f === 'active' ? '未完了' : '完了'}
          </button>
        ))}
      </div>
      
      <ul className="task-list">
        {filteredTasks.map(task => (
          <li
            key={task.id}
            className={task.completed ? 'completed' : ''}
            onClick={() => toggleTask(task.id)}
          >
            {task.title}
          </li>
        ))}
      </ul>
      
      <p>残り: {tasks.filter(t => !t.completed).length} タスク</p>
    </div>
  );
};

useEffect – 副作用の管理

import React, { useState, useEffect, useRef } from 'react';

interface Post {
  id: number;
  title: string;
  body: string;
}

const DataFetcher: React.FC = () => {
  const [posts, setPosts] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const abortControllerRef = useRef(null);

  useEffect(() => {
    // クリーンアップのためAbortControllerを使用
    abortControllerRef.current = new AbortController();
    
    const fetchPosts = async () => {
      try {
        setLoading(true);
        const response = await fetch(
          'https://jsonplaceholder.typicode.com/posts?_limit=10',
          { signal: abortControllerRef.current!.signal }
        );
        
        if (!response.ok) throw new Error('データの取得に失敗しました');
        
        const data = await response.json();
        setPosts(data);
      } catch (err) {
        if (err instanceof Error && err.name !== 'AbortError') {
          setError(err.message);
        }
      } finally {
        setLoading(false);
      }
    };

    fetchPosts();

    // クリーンアップ関数
    return () => {
      abortControllerRef.current?.abort();
    };
  }, []); // 空配列 = マウント時のみ実行

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

  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>
          <h3>{post.title}</h3>
          <p>{post.body}</p>
        </li>
      ))}
    </ul>
  );
};

useContext – グローバル状態管理

import React, { createContext, useContext, useState, ReactNode } from 'react';

// テーマのコンテキスト定義
interface ThemeContextType {
  theme: 'light' | 'dark';
  toggleTheme: () => void;
}

const ThemeContext = createContext(undefined);

// カスタムフック
const useTheme = () => {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme must be used within ThemeProvider');
  }
  return context;
};

// プロバイダーコンポーネント
export const ThemeProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
  const [theme, setTheme] = useState<'light' | 'dark'>('light');

  const toggleTheme = () => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light');
  };

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      <div className={theme}>{children}</div>
    </ThemeContext.Provider>
  );
};

// 使用例
const Header: React.FC = () => {
  const { theme, toggleTheme } = useTheme();
  
  return (
    <header>
      <h1>Tech Athletes</h1>
      <button onClick={toggleTheme}>
        {theme === 'light' ? '🌙 ダークモード' : '☀️ ライトモード'}
      </button>
    </header>
  );
};

カスタムHooksで再利用可能なロジックを作る

import { useState, useCallback, useEffect } from 'react';

// ローカルストレージとの同期
function useLocalStorage(key: string, initialValue: T) {
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch {
      return initialValue;
    }
  });

  const setValue = useCallback((value: T | ((val: T) => T)) => {
    try {
      const valueToStore = value instanceof Function ? value(storedValue) : value;
      setStoredValue(valueToStore);
      window.localStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) {
      console.error(error);
    }
  }, [key, storedValue]);

  return [storedValue, setValue] as const;
}

// デバウンス
function useDebounce(value: T, delay: number): T {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => clearTimeout(handler);
  }, [value, delay]);

  return debouncedValue;
}

// 使用例
const SearchComponent: React.FC = () => {
  const [searchTerm, setSearchTerm] = useLocalStorage('lastSearch', '');
  const debouncedSearch = useDebounce(searchTerm, 500);

  useEffect(() => {
    if (debouncedSearch) {
      console.log('検索実行:', debouncedSearch);
    }
  }, [debouncedSearch]);

  return (
    <input
      value={searchTerm}
      onChange={e => setSearchTerm(e.target.value)}
      placeholder="検索(500ms後に実行)"
    />
  );
};

Reactパフォーマンス最適化

React.memo・useMemo・useCallbackの使い方

import React, { useState, useMemo, useCallback, memo } from 'react';

interface Product {
  id: number;
  name: string;
  price: number;
  category: string;
}

// React.memoで不要な再レンダリングを防ぐ
const ProductCard = memo<{ product: Product; onAddToCart: (id: number) => void }>(
  ({ product, onAddToCart }) => {
    console.log('ProductCard rendered:', product.id);
    return (
      <div className="product-card">
        <h3>{product.name}</h3>
        <p>¥{product.price.toLocaleString()}</p>
        <button onClick={() => onAddToCart(product.id)}>カートに追加</button>
      </div>
    );
  }
);

const ProductList: React.FC = () => {
  const [products] = useState([
    { id: 1, name: 'MacBook Pro', price: 248800, category: 'PC' },
    { id: 2, name: 'iPhone 17', price: 124800, category: 'スマホ' },
    { id: 3, name: 'AirPods Pro', price: 39800, category: 'イヤホン' },
  ]);
  
  const [cart, setCart] = useState([]);
  const [filterCategory, setFilterCategory] = useState('');

  // useMemo: 重い計算のメモ化
  const filteredProducts = useMemo(() => {
    return filterCategory
      ? products.filter(p => p.category === filterCategory)
      : products;
  }, [products, filterCategory]);

  const totalPrice = useMemo(() => {
    return cart.reduce((sum, id) => {
      const product = products.find(p => p.id === id);
      return sum + (product?.price || 0);
    }, 0);
  }, [cart, products]);

  // useCallback: 関数のメモ化
  const handleAddToCart = useCallback((id: number) => {
    setCart(prev => [...prev, id]);
  }, []);

  return (
    <div>
      <p>カート合計: ¥{totalPrice.toLocaleString()}</p>
      {filteredProducts.map(product => (
        <ProductCard
          key={product.id}
          product={product}
          onAddToCart={handleAddToCart}
        />
      ))}
    </div>
  );
};

Reactのテスト(Vitest + Testing Library)

import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { vi, describe, it, expect, beforeEach } from 'vitest';
import userEvent from '@testing-library/user-event';
import { TodoApp } from './TodoApp';

describe('TodoApp', () => {
  it('初期状態でタスクリストが空であること', () => {
    render(<TodoApp />);
    expect(screen.getByText('残り: 0 タスク')).toBeInTheDocument();
  });

  it('新しいタスクを追加できること', async () => {
    const user = userEvent.setup();
    render(<TodoApp />);
    
    const input = screen.getByPlaceholderText('タスクを入力...');
    await user.type(input, '新しいタスク');
    await user.click(screen.getByText('追加'));
    
    expect(screen.getByText('新しいタスク')).toBeInTheDocument();
    expect(screen.getByText('残り: 1 タスク')).toBeInTheDocument();
  });

  it('タスクをクリックで完了状態にできること', async () => {
    const user = userEvent.setup();
    render(<TodoApp />);
    
    // タスクを追加
    const input = screen.getByPlaceholderText('タスクを入力...');
    await user.type(input, 'テストタスク');
    await user.click(screen.getByText('追加'));
    
    // タスクをクリックして完了にする
    await user.click(screen.getByText('テストタスク'));
    expect(screen.getByText('残り: 0 タスク')).toBeInTheDocument();
  });
});

まとめ:React習得のロードマップ

ステップ学習内容期間の目安
Step 1JavaScript・TypeScript基礎1〜2ヶ月
Step 2React基礎(JSX、Props、State)1〜2ヶ月
Step 3Hooks完全習得2〜3ヶ月
Step 4状態管理(Zustand/Jotai)1〜2ヶ月
Step 5Next.js(SSR/SSG)2〜3ヶ月
Step 6テスト・パフォーマンス最適化2〜3ヶ月

Reactは2026年も最前線で使われているフロントエンドライブラリです。本記事で紹介した知識を身につけ、実際にプロジェクトを作りながら習得していきましょう。

投稿者 kasata

IT企業でエンジニアとして勤務後、テクノロジー情報メディア「Tech Athletes(テック・アスリート)」を運営。プログラミング、クラウドインフラ(AWS/GCP/Azure)、AI活用、Webサービス開発を専門とする。エンジニア・ビジネスパーソン向けに、実際に使ってみた経験をもとに信頼できる技術情報を発信中。資格:AWS認定ソリューションアーキテクト、Python 3 エンジニア認定試験合格。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

Click to listen highlighted text!