Reactでできること一覧 - モダンWeb開発の可能性を解説

Reactで構築できるアプリケーションを網羅的に解説。Webアプリ、モバイルアプリ、デスクトップアプリまで、Reactの無限の可能性を紹介

Learning Next 運営
84 分で読めます

みなさん、Reactを学んだら実際に何が作れるか知っていますか?

「ReactってWebサイトを作るだけでしょ?」 「どこまでの可能性があるの?」

こんな疑問を持っている方も多いと思います。

実は、Reactは単なるWebライブラリを超えて、モバイルアプリからデスクトップアプリまで、あらゆるデジタル体験を創造できる万能ツールなんです。

この記事では、Reactで作れるアプリケーションを具体的なコード例とともに詳しく解説します。 きっと、Reactの可能性の広さに驚かれることでしょう。

Reactの基本的な活用範囲

まず、Reactがどんな分野で活用できるかを全体的に見てみましょう。

Reactが対応する開発領域

Reactは想像以上に幅広い分野で活用されています。

主要な開発分野

  • Webアプリケーション: SPA、PWA、静的サイト
  • モバイルアプリ: React Native による iOS/Android アプリ
  • デスクトップアプリ: Electron による Windows/Mac/Linux アプリ
  • サーバーサイドレンダリング: Next.js による高性能Webサイト
  • VR/AR: React 360、React VR による没入型体験
// Reactの基本構造:どの分野でも共通の考え方
function App() {
  return (
    <div className="app">
      <Header />
      <MainContent />
      <Footer />
    </div>
  );
}

この例では、Reactの基本的なコンポーネント構造を示しています。 AppコンポーネントがHeaderMainContentFooterを組み合わせて作られています。

このコンポーネントベースの設計により、どのプラットフォームでも同じ考え方で開発できます。

Reactエコシステムの威力

Reactの真の強さは、豊富なエコシステムにあります。

主要なReactライブラリ

  • React Router: ルーティング管理
  • Redux/Zustand: 状態管理
  • Material-UI/Chakra UI: UIコンポーネント
  • React Hook Form: フォーム管理
  • React Query: データフェッチング
  • Framer Motion: アニメーション

これらのライブラリを組み合わせることで、企業レベルのアプリケーションを効率的に開発できます。

Webアプリケーション開発

Reactの最も基本的で強力な活用分野がWebアプリケーション開発です。

シングルページアプリケーション(SPA)

本格的なECサイトを作る場合の全体構成を見てみましょう。

import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { useState, useEffect } from 'react';

// 本格的なECサイトの例
function ECommerceApp() {
  const [user, setUser] = useState(null);
  const [cart, setCart] = useState([]);
  const [products, setProducts] = useState([]);

  return (
    <Router>
      <div className="ecommerce-app">
        <Header user={user} cartItems={cart.length} />
        
        <Routes>
          <Route path="/" element={<HomePage products={products} />} />
          <Route path="/products" element={<ProductsPage />} />
          <Route path="/products/:id" element={<ProductDetailPage />} />
          <Route path="/cart" element={<CartPage cart={cart} setCart={setCart} />} />
          <Route path="/checkout" element={<CheckoutPage />} />
          <Route path="/profile" element={<ProfilePage user={user} />} />
        </Routes>
        
        <Footer />
      </div>
    </Router>
  );
}

このコードでは、ECサイトの基本的な構造を定義しています。 BrowserRouterでルーティングを管理し、Routesで各ページを設定しています。

useStateを使って、ユーザー情報、カート、商品データを管理しています。 これらの状態は、アプリ全体で共有されます。

次に、商品一覧ページの詳細を見てみましょう。

// 商品一覧ページ
function ProductsPage() {
  const [products, setProducts] = useState([]);
  const [filters, setFilters] = useState({
    category: 'all',
    priceRange: [0, 10000],
    sortBy: 'name'
  });
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetchProducts();
  }, [filters]);

  const fetchProducts = async () => {
    try {
      setLoading(true);
      const response = await fetch('/api/products', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(filters)
      });
      const data = await response.json();
      setProducts(data);
    } catch (error) {
      console.error('商品取得エラー:', error);
    } finally {
      setLoading(false);
    }
  };

  return (
    <div className="products-page">
      <aside className="filters-sidebar">
        <ProductFilters filters={filters} setFilters={setFilters} />
      </aside>
      
      <main className="products-main">
        {loading ? (
          <ProductsLoading />
        ) : (
          <div className="products-grid">
            {products.map(product => (
              <ProductCard key={product.id} product={product} />
            ))}
          </div>
        )}
      </main>
    </div>
  );
}

このページでは、商品の取得とフィルタリング機能を実装しています。 useEffectを使って、フィルターが変更されるたびに商品データを再取得しています。

fetchProducts関数では、APIにPOSTリクエストを送信してフィルター条件を送っています。 try-catch文でエラーハンドリングも行っています。

商品カードコンポーネントも見てみましょう。

// 商品カードコンポーネント
function ProductCard({ product }) {
  const [liked, setLiked] = useState(false);
  const [addingToCart, setAddingToCart] = useState(false);

  const handleAddToCart = async () => {
    setAddingToCart(true);
    try {
      await fetch('/api/cart', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ productId: product.id, quantity: 1 })
      });
      
      // カート追加成功の通知
      showNotification('カートに追加しました');
    } catch (error) {
      showNotification('エラーが発生しました', 'error');
    } finally {
      setAddingToCart(false);
    }
  };

  return (
    <div className="product-card">
      <div className="product-image">
        <img src={product.images[0]} alt={product.name} />
        <button 
          className={`like-button ${liked ? 'liked' : ''}`}
          onClick={() => setLiked(!liked)}
        >
          ❤️
        </button>
      </div>
      
      <div className="product-info">
        <h3>{product.name}</h3>
        <p className="price">¥{product.price.toLocaleString()}</p>
        <p className="description">{product.description}</p>
        
        <div className="product-actions">
          <button 
            className="add-to-cart"
            onClick={handleAddToCart}
            disabled={addingToCart}
          >
            {addingToCart ? 'カートに追加中...' : 'カートに追加'}
          </button>
          
          <Link to={`/products/${product.id}`} className="view-details">
            詳細を見る
          </Link>
        </div>
      </div>
    </div>
  );
}

このコンポーネントでは、商品の表示とカート追加機能を実装しています。 liked状態でお気に入り機能を管理しています。

handleAddToCart関数では、カートに商品を追加するAPIリクエストを送信しています。 addingToCart状態でボタンの無効化とローディング表示を行っています。

プログレッシブWebアプリ(PWA)

PWAの実装例も見てみましょう。

// Service Worker対応のPWA
import { useState, useEffect } from 'react';

function NewsApp() {
  const [articles, setArticles] = useState([]);
  const [isOnline, setIsOnline] = useState(navigator.onLine);
  const [installPrompt, setInstallPrompt] = useState(null);

  useEffect(() => {
    // オンライン/オフライン状態の監視
    const handleOnline = () => setIsOnline(true);
    const handleOffline = () => setIsOnline(false);
    
    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);

    // PWAインストールプロンプト
    const handleBeforeInstallPrompt = (e) => {
      e.preventDefault();
      setInstallPrompt(e);
    };

    window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt);

    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
      window.removeEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
    };
  }, []);

  const handleInstallPWA = async () => {
    if (installPrompt) {
      installPrompt.prompt();
      const result = await installPrompt.userChoice;
      if (result.outcome === 'accepted') {
        setInstallPrompt(null);
      }
    }
  };

  return (
    <div className="news-app">
      {/* PWAインストールバナー */}
      {installPrompt && (
        <div className="install-banner">
          <p>このアプリをホーム画面に追加しますか?</p>
          <button onClick={handleInstallPWA}>インストール</button>
          <button onClick={() => setInstallPrompt(null)}>後で</button>
        </div>
      )}

      {/* オフライン状態の表示 */}
      {!isOnline && (
        <div className="offline-banner">
          <p>オフラインモードです。一部機能が制限されます。</p>
        </div>
      )}

      <Header />
      <ArticleList articles={articles} isOnline={isOnline} />
    </div>
  );
}

この例では、PWAの基本的な機能を実装しています。

navigator.onLineでオンライン状態を監視しています。 beforeinstallpromptイベントでPWAのインストールプロンプトを捕捉しています。

handleInstallPWA関数では、インストールプロンプトを表示して、ユーザーの選択を待っています。

オフライン対応の記事リストも見てみましょう。

// オフライン対応記事リスト
function ArticleList({ articles, isOnline }) {
  const [cachedArticles, setCachedArticles] = useState([]);

  useEffect(() => {
    if (isOnline) {
      fetchArticles();
    } else {
      loadCachedArticles();
    }
  }, [isOnline]);

  const fetchArticles = async () => {
    try {
      const response = await fetch('/api/articles');
      const data = await response.json();
      setArticles(data);
      
      // IndexedDBにキャッシュ
      cacheArticles(data);
    } catch (error) {
      console.error('記事取得エラー:', error);
      loadCachedArticles();
    }
  };

  const cacheArticles = async (articles) => {
    // IndexedDBへの保存処理
    const db = await openDB('newsApp', 1);
    const tx = db.transaction('articles', 'readwrite');
    articles.forEach(article => {
      tx.objectStore('articles').put(article);
    });
  };

  return (
    <div className="article-list">
      {(isOnline ? articles : cachedArticles).map(article => (
        <ArticleCard key={article.id} article={article} isOnline={isOnline} />
      ))}
    </div>
  );
}

このコンポーネントでは、オンライン時は最新の記事を取得し、オフライン時はキャッシュされた記事を表示しています。

cacheArticles関数では、IndexedDBに記事データを保存しています。 これにより、オフライン時でも記事を閲覧できます。

ダッシュボード・管理画面

高機能なダッシュボードの例も見てみましょう。

import { useState, useEffect } from 'react';
import { LineChart, BarChart, PieChart, ResponsiveContainer } from 'recharts';

// 高機能なダッシュボード
function AdminDashboard() {
  const [stats, setStats] = useState({});
  const [chartData, setChartData] = useState([]);
  const [users, setUsers] = useState([]);
  const [selectedPeriod, setSelectedPeriod] = useState('7days');

  useEffect(() => {
    fetchDashboardData();
  }, [selectedPeriod]);

  const fetchDashboardData = async () => {
    try {
      const [statsRes, chartRes, usersRes] = await Promise.all([
        fetch(`/api/stats?period=${selectedPeriod}`),
        fetch(`/api/analytics?period=${selectedPeriod}`),
        fetch('/api/users?limit=10')
      ]);

      setStats(await statsRes.json());
      setChartData(await chartRes.json());
      setUsers(await usersRes.json());
    } catch (error) {
      console.error('ダッシュボードデータ取得エラー:', error);
    }
  };

  return (
    <div className="admin-dashboard">
      <header className="dashboard-header">
        <h1>管理ダッシュボード</h1>
        <div className="period-selector">
          <select 
            value={selectedPeriod} 
            onChange={(e) => setSelectedPeriod(e.target.value)}
          >
            <option value="24hours">過去24時間</option>
            <option value="7days">過去7日間</option>
            <option value="30days">過去30日間</option>
            <option value="90days">過去90日間</option>
          </select>
        </div>
      </header>

      {/* KPI カード */}
      <div className="stats-grid">
        <StatCard 
          title="総ユーザー数" 
          value={stats.totalUsers} 
          change={stats.userGrowth}
          icon="👥"
        />
        <StatCard 
          title="売上" 
          value={`¥${stats.revenue?.toLocaleString()}`} 
          change={stats.revenueGrowth}
          icon="💰"
        />
        <StatCard 
          title="注文数" 
          value={stats.orders} 
          change={stats.orderGrowth}
          icon="📦"
        />
        <StatCard 
          title="コンバージョン率" 
          value={`${stats.conversionRate}%`} 
          change={stats.conversionGrowth}
          icon="📈"
        />
      </div>

      {/* チャート */}
      <div className="charts-grid">
        <div className="chart-container">
          <h3>売上推移</h3>
          <ResponsiveContainer width="100%" height={300}>
            <LineChart data={chartData.revenue}>
              <XAxis dataKey="date" />
              <YAxis />
              <Tooltip />
              <Line type="monotone" dataKey="amount" stroke="#8884d8" />
            </LineChart>
          </ResponsiveContainer>
        </div>

        <div className="chart-container">
          <h3>カテゴリ別売上</h3>
          <ResponsiveContainer width="100%" height={300}>
            <PieChart>
              <Pie 
                data={chartData.categories} 
                dataKey="value" 
                nameKey="name"
                cx="50%" 
                cy="50%" 
                outerRadius={80} 
                fill="#8884d8" 
              />
              <Tooltip />
            </PieChart>
          </ResponsiveContainer>
        </div>
      </div>
    </div>
  );
}

このダッシュボードでは、KPIカードとチャートを組み合わせた管理画面を実装しています。

Promise.allを使って複数のAPIを同時に呼び出し、効率的にデータを取得しています。 selectedPeriodの変更に応じて、自動的にデータが更新されます。

チャートライブラリにはrechartsを使用しており、レスポンシブなグラフを簡単に作成できます。

モバイルアプリ開発(React Native)

React Nativeを使うと、Reactの知識でモバイルアプリを開発できます。

iOS/Androidアプリ

モバイルアプリの基本構造を見てみましょう。

// React Nativeによるモバイルアプリ
import React, { useState, useEffect } from 'react';
import { 
  View, 
  Text, 
  FlatList, 
  TouchableOpacity, 
  Image, 
  Alert,
  StyleSheet 
} from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';

const Tab = createBottomTabNavigator();

// メインアプリケーション
function MobileApp() {
  return (
    <NavigationContainer>
      <Tab.Navigator>
        <Tab.Screen 
          name="Home" 
          component={HomeScreen} 
          options={{
            tabBarIcon: ({ color, size }) => (
              <Icon name="home" color={color} size={size} />
            )
          }}
        />
        <Tab.Screen 
          name="Products" 
          component={ProductsScreen}
          options={{
            tabBarIcon: ({ color, size }) => (
              <Icon name="shopping-bag" color={color} size={size} />
            )
          }}
        />
        <Tab.Screen 
          name="Cart" 
          component={CartScreen}
          options={{
            tabBarIcon: ({ color, size }) => (
              <Icon name="shopping-cart" color={color} size={size} />
            )
          }}
        />
        <Tab.Screen 
          name="Profile" 
          component={ProfileScreen}
          options={{
            tabBarIcon: ({ color, size }) => (
              <Icon name="user" color={color} size={size} />
            )
          }}
        />
      </Tab.Navigator>
    </NavigationContainer>
  );
}

この例では、React Nativeでタブナビゲーションを実装しています。 createBottomTabNavigatorで下部タブを作成し、各画面を配置しています。

WebのReactとの違いは、divの代わりにViewpの代わりにTextを使うことです。

ホーム画面の詳細も見てみましょう。

// ホーム画面
function HomeScreen() {
  const [featuredProducts, setFeaturedProducts] = useState([]);
  const [categories, setCategories] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetchHomeData();
  }, []);

  const fetchHomeData = async () => {
    try {
      const [productsRes, categoriesRes] = await Promise.all([
        fetch('/api/products/featured'),
        fetch('/api/categories')
      ]);

      setFeaturedProducts(await productsRes.json());
      setCategories(await categoriesRes.json());
    } catch (error) {
      Alert.alert('エラー', 'データの取得に失敗しました');
    } finally {
      setLoading(false);
    }
  };

  return (
    <View style={styles.container}>
      <Text style={styles.title}>おすすめ商品</Text>
      
      {loading ? (
        <Text>読み込み中...</Text>
      ) : (
        <>
          <FlatList
            data={featuredProducts}
            renderItem={({ item }) => <ProductCard product={item} />}
            keyExtractor={item => item.id.toString()}
            horizontal
            showsHorizontalScrollIndicator={false}
            style={styles.productsList}
          />

          <Text style={styles.sectionTitle}>カテゴリ</Text>
          <FlatList
            data={categories}
            renderItem={({ item }) => <CategoryCard category={item} />}
            keyExtractor={item => item.id.toString()}
            numColumns={2}
            style={styles.categoriesList}
          />
        </>
      )}
    </View>
  );
}

ホーム画面では、おすすめ商品とカテゴリを表示しています。 FlatListでリストを表示し、horizontalプロパティで横スクロールを実現しています。

エラーハンドリングにはAlert.alertを使用しており、ネイティブのアラートダイアログが表示されます。

商品カードコンポーネントも見てみましょう。

// 商品カードコンポーネント
function ProductCard({ product }) {
  const handlePress = () => {
    // 商品詳細画面に遷移
    navigation.navigate('ProductDetail', { productId: product.id });
  };

  return (
    <TouchableOpacity style={styles.productCard} onPress={handlePress}>
      <Image source={{ uri: product.image }} style={styles.productImage} />
      <View style={styles.productInfo}>
        <Text style={styles.productName} numberOfLines={2}>
          {product.name}
        </Text>
        <Text style={styles.productPrice}>
          ¥{product.price.toLocaleString()}
        </Text>
        <View style={styles.productRating}>
          <Text>⭐ {product.rating}</Text>
          <Text style={styles.reviewCount}>({product.reviewCount})</Text>
        </View>
      </View>
    </TouchableOpacity>
  );
}

モバイルアプリでは、TouchableOpacityでタップ可能な要素を作成します。 numberOfLinesプロパティで、テキストの行数を制限できます。

スタイル定義も見てみましょう。

// スタイル定義
const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    padding: 16,
  },
  title: {
    fontSize: 24,
    fontWeight: 'bold',
    marginBottom: 16,
  },
  productCard: {
    backgroundColor: '#f9f9f9',
    borderRadius: 8,
    padding: 12,
    marginRight: 12,
    width: 200,
  },
  productImage: {
    width: '100%',
    height: 120,
    borderRadius: 8,
    marginBottom: 8,
  },
  productName: {
    fontSize: 16,
    fontWeight: '600',
    marginBottom: 4,
  },
  productPrice: {
    fontSize: 18,
    fontWeight: 'bold',
    color: '#e74c3c',
    marginBottom: 4,
  },
  productRating: {
    flexDirection: 'row',
    alignItems: 'center',
  },
});

React Nativeでは、StyleSheet.createでスタイルを定義します。 CSSと似ていますが、flexDirectionalignItemsなどのFlexboxプロパティを使用します。

ネイティブ機能の活用

React Nativeでは、カメラや位置情報などのネイティブ機能も使用できます。

// カメラ、位置情報、プッシュ通知などの活用
import { Camera } from 'expo-camera';
import * as Location from 'expo-location';
import * as Notifications from 'expo-notifications';

function CameraScreen() {
  const [hasPermission, setHasPermission] = useState(null);
  const [type, setType] = useState(Camera.Constants.Type.back);
  const [photos, setPhotos] = useState([]);

  useEffect(() => {
    (async () => {
      const { status } = await Camera.requestCameraPermissionsAsync();
      setHasPermission(status === 'granted');
    })();
  }, []);

  const takePicture = async () => {
    if (cameraRef) {
      const photo = await cameraRef.current.takePictureAsync();
      setPhotos(prev => [...prev, photo]);
      
      // 写真をサーバーにアップロード
      uploadPhoto(photo);
    }
  };

  const uploadPhoto = async (photo) => {
    const formData = new FormData();
    formData.append('photo', {
      uri: photo.uri,
      type: 'image/jpeg',
      name: 'photo.jpg',
    });

    try {
      await fetch('/api/upload', {
        method: 'POST',
        body: formData,
        headers: {
          'Content-Type': 'multipart/form-data',
        },
      });
    } catch (error) {
      Alert.alert('エラー', '写真のアップロードに失敗しました');
    }
  };

  if (hasPermission === null) {
    return <View />;
  }
  if (hasPermission === false) {
    return <Text>カメラへのアクセスが拒否されました</Text>;
  }

  return (
    <View style={styles.cameraContainer}>
      <Camera style={styles.camera} type={type} ref={cameraRef}>
        <View style={styles.buttonContainer}>
          <TouchableOpacity
            style={styles.button}
            onPress={() => {
              setType(
                type === Camera.Constants.Type.back
                  ? Camera.Constants.Type.front
                  : Camera.Constants.Type.back
              );
            }}>
            <Text style={styles.text}>カメラ切り替え</Text>
          </TouchableOpacity>
          
          <TouchableOpacity style={styles.captureButton} onPress={takePicture}>
            <Text style={styles.text}>撮影</Text>
          </TouchableOpacity>
        </View>
      </Camera>
    </View>
  );
}

このカメラ機能では、Camera.requestCameraPermissionsAsyncで権限を要求しています。 takePictureAsyncで写真を撮影し、FormDataを使ってサーバーにアップロードしています。

位置情報機能も見てみましょう。

// 位置情報機能
function LocationScreen() {
  const [location, setLocation] = useState(null);
  const [nearbyStores, setNearbyStores] = useState([]);

  useEffect(() => {
    getCurrentLocation();
  }, []);

  const getCurrentLocation = async () => {
    const { status } = await Location.requestForegroundPermissionsAsync();
    if (status !== 'granted') {
      Alert.alert('エラー', '位置情報へのアクセスが拒否されました');
      return;
    }

    const location = await Location.getCurrentPositionAsync({});
    setLocation(location);
    
    // 近くの店舗を検索
    findNearbyStores(location.coords);
  };

  const findNearbyStores = async (coords) => {
    try {
      const response = await fetch(
        `/api/stores/nearby?lat=${coords.latitude}&lng=${coords.longitude}&radius=5000`
      );
      const stores = await response.json();
      setNearbyStores(stores);
    } catch (error) {
      console.error('店舗検索エラー:', error);
    }
  };

  return (
    <View style={styles.container}>
      <Text style={styles.title}>近くの店舗</Text>
      
      {location && (
        <Text style={styles.locationText}>
          現在地: {location.coords.latitude.toFixed(4)}, {location.coords.longitude.toFixed(4)}
        </Text>
      )}

      <FlatList
        data={nearbyStores}
        renderItem={({ item }) => (
          <View style={styles.storeCard}>
            <Text style={styles.storeName}>{item.name}</Text>
            <Text style={styles.storeAddress}>{item.address}</Text>
            <Text style={styles.storeDistance}>{item.distance}m</Text>
          </View>
        )}
        keyExtractor={item => item.id.toString()}
      />
    </View>
  );
}

位置情報機能では、Location.getCurrentPositionAsyncで現在位置を取得しています。 取得した座標を使って、近くの店舗を検索するAPIを呼び出しています。

デスクトップアプリ開発(Electron)

ElectronとReactを組み合わせて、デスクトップアプリを開発できます。

ネイティブデスクトップアプリ

まず、Electronのメインプロセスを見てみましょう。

// Electronメインプロセス(main.js)
const { app, BrowserWindow, Menu, ipcMain, dialog } = require('electron');
const path = require('path');

function createWindow() {
  const mainWindow = new BrowserWindow({
    width: 1200,
    height: 800,
    webPreferences: {
      nodeIntegration: true,
      contextIsolation: false
    }
  });

  // React アプリを読み込み
  const isDev = process.env.NODE_ENV === 'development';
  if (isDev) {
    mainWindow.loadURL('http://localhost:3000');
    mainWindow.webContents.openDevTools();
  } else {
    mainWindow.loadFile(path.join(__dirname, '../build/index.html'));
  }

  // メニューバーの設定
  const template = [
    {
      label: 'ファイル',
      submenu: [
        {
          label: '新規作成',
          accelerator: 'CmdOrCtrl+N',
          click: () => {
            mainWindow.webContents.send('menu-new-file');
          }
        },
        {
          label: '開く',
          accelerator: 'CmdOrCtrl+O',
          click: async () => {
            const result = await dialog.showOpenDialog(mainWindow, {
              properties: ['openFile'],
              filters: [
                { name: 'Text Files', extensions: ['txt', 'md'] },
                { name: 'All Files', extensions: ['*'] }
              ]
            });

            if (!result.canceled) {
              mainWindow.webContents.send('menu-open-file', result.filePaths[0]);
            }
          }
        },
        {
          label: '保存',
          accelerator: 'CmdOrCtrl+S',
          click: () => {
            mainWindow.webContents.send('menu-save-file');
          }
        }
      ]
    },
    {
      label: '編集',
      submenu: [
        { role: 'undo', label: '元に戻す' },
        { role: 'redo', label: 'やり直し' },
        { type: 'separator' },
        { role: 'cut', label: '切り取り' },
        { role: 'copy', label: 'コピー' },
        { role: 'paste', label: '貼り付け' }
      ]
    }
  ];

  const menu = Menu.buildFromTemplate(template);
  Menu.setApplicationMenu(menu);
}

app.whenReady().then(createWindow);

このコードでは、Electronでデスクトップアプリのウィンドウを作成しています。 BrowserWindowでアプリのウィンドウを作成し、Reactアプリを読み込んでいます。

メニューバーも設定しており、「ファイル」「編集」メニューを作成しています。 各メニューアイテムには、キーボードショートカットも設定しています。

IPCハンドラーも設定してみましょう。

// IPCハンドラー
ipcMain.handle('save-file', async (event, filePath, content) => {
  const fs = require('fs').promises;
  try {
    await fs.writeFile(filePath, content, 'utf8');
    return { success: true };
  } catch (error) {
    return { success: false, error: error.message };
  }
});

ipcMain.handle('read-file', async (event, filePath) => {
  const fs = require('fs').promises;
  try {
    const content = await fs.readFile(filePath, 'utf8');
    return { success: true, content };
  } catch (error) {
    return { success: false, error: error.message };
  }
});

IPCハンドラーでは、ファイルの読み書き処理を実装しています。 ipcMain.handleで、レンダラープロセスからの要求を処理しています。

次に、Reactテキストエディターアプリを見てみましょう。

// React テキストエディターアプリ
import { useState, useEffect } from 'react';
const { ipcRenderer } = window.require('electron');

function TextEditorApp() {
  const [content, setContent] = useState('');
  const [filePath, setFilePath] = useState(null);
  const [isModified, setIsModified] = useState(false);
  const [fontSize, setFontSize] = useState(14);

  useEffect(() => {
    // Electronメニューからのイベントリスナー
    ipcRenderer.on('menu-new-file', handleNewFile);
    ipcRenderer.on('menu-open-file', handleOpenFile);
    ipcRenderer.on('menu-save-file', handleSaveFile);

    return () => {
      ipcRenderer.removeAllListeners('menu-new-file');
      ipcRenderer.removeAllListeners('menu-open-file');
      ipcRenderer.removeAllListeners('menu-save-file');
    };
  }, []);

  const handleNewFile = () => {
    if (isModified) {
      const result = confirm('変更が保存されていません。新規作成しますか?');
      if (!result) return;
    }
    
    setContent('');
    setFilePath(null);
    setIsModified(false);
  };

  const handleOpenFile = async (event, selectedFilePath) => {
    try {
      const result = await ipcRenderer.invoke('read-file', selectedFilePath);
      if (result.success) {
        setContent(result.content);
        setFilePath(selectedFilePath);
        setIsModified(false);
      } else {
        alert(`ファイルの読み込みに失敗しました: ${result.error}`);
      }
    } catch (error) {
      alert(`エラーが発生しました: ${error.message}`);
    }
  };

  const handleSaveFile = async () => {
    if (!filePath) {
      // 名前を付けて保存
      const result = await ipcRenderer.invoke('show-save-dialog');
      if (result.canceled) return;
      setFilePath(result.filePath);
    }

    try {
      const result = await ipcRenderer.invoke('save-file', filePath, content);
      if (result.success) {
        setIsModified(false);
        alert('ファイルを保存しました');
      } else {
        alert(`保存に失敗しました: ${result.error}`);
      }
    } catch (error) {
      alert(`エラーが発生しました: ${error.message}`);
    }
  };

  const handleContentChange = (newContent) => {
    setContent(newContent);
    setIsModified(true);
  };

  return (
    <div className="text-editor">
      <header className="editor-header">
        <div className="file-info">
          <h1>{filePath ? filePath.split('/').pop() : '無題'}</h1>
          {isModified && <span className="modified-indicator">●</span>}
        </div>
        
        <div className="editor-controls">
          <label>
            フォントサイズ:
            <input
              type="range"
              min="10"
              max="24"
              value={fontSize}
              onChange={(e) => setFontSize(Number(e.target.value))}
            />
            {fontSize}px
          </label>
          
          <button onClick={handleNewFile}>新規</button>
          <button onClick={handleSaveFile}>保存</button>
        </div>
      </header>

      <main className="editor-main">
        <textarea
          value={content}
          onChange={(e) => handleContentChange(e.target.value)}
          style={{ fontSize: `${fontSize}px` }}
          className="editor-textarea"
          placeholder="ここに文章を入力してください..."
        />
      </main>

      <footer className="editor-footer">
        <div className="stats">
          文字数: {content.length} | 
          行数: {content.split('
').length} |
          単語数: {content.split(/\s+/).filter(word => word.length > 0).length}
        </div>
      </footer>
    </div>
  );
}

export default TextEditorApp;

このテキストエディターでは、Electronのメニューからのイベントを受信しています。 ipcRenderer.onでメニューイベントを監視し、ipcRenderer.invokeでメインプロセスの機能を呼び出しています。

ファイルの保存・読み込み・新規作成機能を実装しており、変更状態の管理も行っています。

サーバーサイドレンダリング(Next.js)

Next.jsを使うと、Reactでサーバーサイドレンダリングを実現できます。

高性能Webサイト

Next.jsでのブログサイトの例を見てみましょう。

// pages/index.js - Next.jsホームページ
import { useState, useEffect } from 'react';
import Head from 'next/head';
import Image from 'next/image';
import Link from 'next/link';

export default function HomePage({ featuredPosts, categories }) {
  return (
    <>
      <Head>
        <title>Tech Blog - 最新の技術情報</title>
        <meta name="description" content="プログラミングから最新技術まで、役立つ情報を発信" />
        <meta property="og:title" content="Tech Blog" />
        <meta property="og:description" content="最新の技術情報をお届け" />
        <meta property="og:image" content="/og-image.jpg" />
      </Head>

      <main className="home-page">
        <section className="hero">
          <div className="hero-content">
            <h1>最新の技術情報をお届け</h1>
            <p>プログラミングから最新技術まで、役立つ情報を発信しています</p>
            <Link href="/posts" className="cta-button">
              記事を読む
            </Link>
          </div>
          <div className="hero-image">
            <Image
              src="/hero-image.jpg"
              alt="Technology"
              width={600}
              height={400}
              priority
            />
          </div>
        </section>

        <section className="featured-posts">
          <h2>注目の記事</h2>
          <div className="posts-grid">
            {featuredPosts.map(post => (
              <article key={post.id} className="post-card">
                <Link href={`/posts/${post.slug}`}>
                  <div className="post-image">
                    <Image
                      src={post.thumbnail}
                      alt={post.title}
                      width={300}
                      height={200}
                    />
                  </div>
                  <div className="post-content">
                    <h3>{post.title}</h3>
                    <p>{post.excerpt}</p>
                    <div className="post-meta">
                      <span>by {post.author.name}</span>
                      <time>{new Date(post.publishedAt).toLocaleDateString()}</time>
                    </div>
                  </div>
                </Link>
              </article>
            ))}
          </div>
        </section>

        <section className="categories">
          <h2>カテゴリ</h2>
          <div className="categories-grid">
            {categories.map(category => (
              <Link key={category.id} href={`/categories/${category.slug}`} className="category-card">
                <div className="category-icon">{category.icon}</div>
                <h3>{category.name}</h3>
                <p>{category.postCount}記事</p>
              </Link>
            ))}
          </div>
        </section>
      </main>
    </>
  );
}

// サーバーサイドでデータを取得
export async function getStaticProps() {
  try {
    const [postsRes, categoriesRes] = await Promise.all([
      fetch(`${process.env.API_URL}/posts?featured=true&limit=6`),
      fetch(`${process.env.API_URL}/categories`)
    ]);

    const featuredPosts = await postsRes.json();
    const categories = await categoriesRes.json();

    return {
      props: {
        featuredPosts,
        categories,
      },
      revalidate: 3600, // 1時間ごとに再生成
    };
  } catch (error) {
    console.error('データ取得エラー:', error);
    return {
      props: {
        featuredPosts: [],
        categories: [],
      },
    };
  }
}

このNext.jsページでは、getStaticPropsでサーバーサイドでデータを取得しています。 Headコンポーネントでメタタグを設定し、SEO対策を行っています。

Imageコンポーネントでは、画像の最適化が自動的に行われます。 priorityプロパティで、重要な画像を優先読み込みできます。

revalidateオプションで、1時間ごとに静的ページを再生成しています。

動的ルーティング

動的な記事ページも見てみましょう。

// pages/posts/[slug].js - 動的な記事ページ
import { useState } from 'react';
import Head from 'next/head';
import Image from 'next/image';
import { useRouter } from 'next/router';

export default function PostPage({ post, relatedPosts }) {
  const router = useRouter();
  const [liked, setLiked] = useState(false);
  const [bookmarked, setBookmarked] = useState(false);

  if (router.isFallback) {
    return <div>読み込み中...</div>;
  }

  const handleShare = async () => {
    if (navigator.share) {
      try {
        await navigator.share({
          title: post.title,
          text: post.excerpt,
          url: window.location.href,
        });
      } catch (error) {
        console.log('シェアがキャンセルされました');
      }
    } else {
      // フォールバック:URLをクリップボードにコピー
      navigator.clipboard.writeText(window.location.href);
      alert('URLをコピーしました');
    }
  };

  return (
    <>
      <Head>
        <title>{post.title} | Tech Blog</title>
        <meta name="description" content={post.excerpt} />
        <meta property="og:title" content={post.title} />
        <meta property="og:description" content={post.excerpt} />
        <meta property="og:image" content={post.thumbnail} />
        <meta property="article:author" content={post.author.name} />
        <meta property="article:published_time" content={post.publishedAt} />
        <meta property="article:tag" content={post.tags.join(', ')} />
      </Head>

      <article className="post-page">
        <header className="post-header">
          <div className="post-meta">
            <Link href={`/categories/${post.category.slug}`} className="category-link">
              {post.category.name}
            </Link>
            <time>{new Date(post.publishedAt).toLocaleDateString()}</time>
          </div>
          
          <h1>{post.title}</h1>
          
          <div className="author-info">
            <Image
              src={post.author.avatar}
              alt={post.author.name}
              width={40}
              height={40}
              className="author-avatar"
            />
            <div>
              <div className="author-name">{post.author.name}</div>
              <div className="author-bio">{post.author.bio}</div>
            </div>
          </div>

          <div className="post-image">
            <Image
              src={post.thumbnail}
              alt={post.title}
              width={800}
              height={400}
              priority
            />
          </div>
        </header>

        <div className="post-content">
          <div 
            className="content-body"
            dangerouslySetInnerHTML={{ __html: post.content }}
          />
        </div>

        <footer className="post-footer">
          <div className="post-tags">
            {post.tags.map(tag => (
              <Link key={tag} href={`/tags/${tag}`} className="tag">
                #{tag}
              </Link>
            ))}
          </div>

          <div className="post-actions">
            <button 
              onClick={() => setLiked(!liked)}
              className={`action-button ${liked ? 'active' : ''}`}
            >
              ❤️ {post.likesCount + (liked ? 1 : 0)}
            </button>
            
            <button 
              onClick={() => setBookmarked(!bookmarked)}
              className={`action-button ${bookmarked ? 'active' : ''}`}
            >
              🔖 ブックマーク
            </button>
            
            <button onClick={handleShare} className="action-button">
              📤 シェア
            </button>
          </div>
        </footer>

        <section className="related-posts">
          <h3>関連記事</h3>
          <div className="related-posts-grid">
            {relatedPosts.map(relatedPost => (
              <Link key={relatedPost.id} href={`/posts/${relatedPost.slug}`} className="related-post">
                <Image
                  src={relatedPost.thumbnail}
                  alt={relatedPost.title}
                  width={200}
                  height={120}
                />
                <h4>{relatedPost.title}</h4>
              </Link>
            ))}
          </div>
        </section>
      </article>
    </>
  );
}

この記事ページでは、navigator.shareAPIを使ってネイティブシェア機能を実装しています。 対応していないブラウザでは、URLをクリップボードにコピーするフォールバック処理を行っています。

記事の詳細な情報もメタタグに設定し、SNSでのシェア時に適切に表示されるようにしています。

静的パスの生成も見てみましょう。

// 静的パスの生成
export async function getStaticPaths() {
  const response = await fetch(`${process.env.API_URL}/posts`);
  const posts = await response.json();

  const paths = posts.map(post => ({
    params: { slug: post.slug }
  }));

  return {
    paths,
    fallback: 'blocking' // 新しい記事も動的に生成
  };
}

// 静的プロパティの生成
export async function getStaticProps({ params }) {
  try {
    const [postRes, relatedRes] = await Promise.all([
      fetch(`${process.env.API_URL}/posts/${params.slug}`),
      fetch(`${process.env.API_URL}/posts/${params.slug}/related`)
    ]);

    const post = await postRes.json();
    const relatedPosts = await relatedRes.json();

    if (!post) {
      return {
        notFound: true,
      };
    }

    return {
      props: {
        post,
        relatedPosts,
      },
      revalidate: 3600,
    };
  } catch (error) {
    return {
      notFound: true,
    };
  }
}

getStaticPathsでは、すべての記事のスラッグを取得して静的パスを生成しています。 fallback: 'blocking'により、新しい記事も動的に生成されます。

getStaticPropsでは、記事の詳細データと関連記事を取得しています。 記事が見つからない場合は、404ページを表示するようになっています。

新しい分野での活用

Reactは従来のWeb開発を超えて、新しい分野でも活用されています。

VR/AR開発

React 360によるVR体験の例を見てみましょう。

// React 360によるVR体験
import React from 'react';
import {
  AppRegistry,
  Environment,
  StyleSheet,
  Text,
  View,
  VrButton,
  asset,
  Sound,
  Animated,
} from 'react-360';

export default class VirtualShowroom extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      selectedProduct: null,
      rotation: new Animated.Value(0),
    };
  }

  selectProduct = (product) => {
    this.setState({ selectedProduct: product });
    
    // 環境を変更
    Environment.setBackgroundImage(
      asset(`backgrounds/${product.environment}.jpg`)
    );

    // 回転アニメーション
    Animated.timing(this.state.rotation, {
      toValue: 360,
      duration: 2000,
    }).start(() => {
      this.state.rotation.setValue(0);
    });

    // 3D音響効果
    Sound.playOneShot({
      source: asset('sounds/select.wav'),
      volume: 0.5,
    });
  };

  render() {
    const rotateY = this.state.rotation.interpolate({
      inputRange: [0, 360],
      outputRange: ['0deg', '360deg'],
    });

    return (
      <View style={styles.panel}>
        <Text style={styles.title}>
          バーチャルショールーム
        </Text>

        <View style={styles.productGrid}>
          {products.map(product => (
            <VrButton
              key={product.id}
              style={styles.productButton}
              onClick={() => this.selectProduct(product)}
            >
              <View style={styles.productCard}>
                <Text style={styles.productName}>{product.name}</Text>
                <Text style={styles.productPrice}>¥{product.price}</Text>
              </View>
            </VrButton>
          ))}
        </View>

        {this.state.selectedProduct && (
          <Animated.View 
            style={[
              styles.selectedProduct,
              { transform: [{ rotateY }] }
            ]}
          >
            <Text style={styles.selectedTitle}>
              {this.state.selectedProduct.name}
            </Text>
            <Text style={styles.selectedDescription}>
              {this.state.selectedProduct.description}
            </Text>
          </Animated.View>
        )}
      </View>
    );
  }
}

このVRアプリでは、仮想的なショールームを作成しています。 VrButtonでVR空間内のボタンを作成し、Environment.setBackgroundImageで背景を動的に変更しています。

Animated.Valueを使って回転アニメーションを実装し、Sound.playOneShotで音響効果も追加しています。

ゲーム開発

Reactベースの2Dゲームも作成できます。

// React ベースの2Dゲーム
import { useState, useEffect, useCallback } from 'react';

function SnakeGame() {
  const [snake, setSnake] = useState([{ x: 10, y: 10 }]);
  const [food, setFood] = useState({ x: 15, y: 15 });
  const [direction, setDirection] = useState({ x: 0, y: 1 });
  const [gameOver, setGameOver] = useState(false);
  const [score, setScore] = useState(0);
  const [gameSpeed, setGameSpeed] = useState(200);

  const BOARD_SIZE = 20;

  // ゲームループ
  useEffect(() => {
    if (gameOver) return;

    const gameInterval = setInterval(() => {
      setSnake(prevSnake => {
        const newSnake = [...prevSnake];
        const head = { ...newSnake[0] };
        
        head.x += direction.x;
        head.y += direction.y;

        // 壁との衝突判定
        if (head.x < 0 || head.x >= BOARD_SIZE || head.y < 0 || head.y >= BOARD_SIZE) {
          setGameOver(true);
          return prevSnake;
        }

        // 自身との衝突判定
        if (newSnake.some(segment => segment.x === head.x && segment.y === head.y)) {
          setGameOver(true);
          return prevSnake;
        }

        newSnake.unshift(head);

        // 食べ物との衝突判定
        if (head.x === food.x && head.y === food.y) {
          setScore(prev => prev + 10);
          setGameSpeed(prev => Math.max(50, prev - 5)); // スピードアップ
          generateFood();
        } else {
          newSnake.pop();
        }

        return newSnake;
      });
    }, gameSpeed);

    return () => clearInterval(gameInterval);
  }, [direction, food, gameOver, gameSpeed]);

  // キーボード操作
  const handleKeyPress = useCallback((e) => {
    if (gameOver) return;

    switch (e.key) {
      case 'ArrowUp':
        if (direction.y !== 1) setDirection({ x: 0, y: -1 });
        break;
      case 'ArrowDown':
        if (direction.y !== -1) setDirection({ x: 0, y: 1 });
        break;
      case 'ArrowLeft':
        if (direction.x !== 1) setDirection({ x: -1, y: 0 });
        break;
      case 'ArrowRight':
        if (direction.x !== -1) setDirection({ x: 1, y: 0 });
        break;
    }
  }, [direction, gameOver]);

  useEffect(() => {
    window.addEventListener('keydown', handleKeyPress);
    return () => window.removeEventListener('keydown', handleKeyPress);
  }, [handleKeyPress]);

  const generateFood = () => {
    let newFood;
    do {
      newFood = {
        x: Math.floor(Math.random() * BOARD_SIZE),
        y: Math.floor(Math.random() * BOARD_SIZE),
      };
    } while (snake.some(segment => segment.x === newFood.x && segment.y === newFood.y));
    
    setFood(newFood);
  };

  const resetGame = () => {
    setSnake([{ x: 10, y: 10 }]);
    setDirection({ x: 0, y: 1 });
    setGameOver(false);
    setScore(0);
    setGameSpeed(200);
    generateFood();
  };

  const renderBoard = () => {
    const board = [];
    for (let y = 0; y < BOARD_SIZE; y++) {
      for (let x = 0; x < BOARD_SIZE; x++) {
        let cellType = 'empty';
        
        if (snake.some(segment => segment.x === x && segment.y === y)) {
          cellType = snake[0].x === x && snake[0].y === y ? 'snake-head' : 'snake-body';
        } else if (food.x === x && food.y === y) {
          cellType = 'food';
        }

        board.push(
          <div
            key={`${x}-${y}`}
            className={`cell ${cellType}`}
            style={{
              gridColumn: x + 1,
              gridRow: y + 1,
            }}
          />
        );
      }
    }
    return board;
  };

  return (
    <div className="snake-game">
      <div className="game-header">
        <h1>スネークゲーム</h1>
        <div className="score">スコア: {score}</div>
        <div className="speed">スピード: {Math.round((200 - gameSpeed) / 10) + 1}</div>
      </div>

      <div 
        className="game-board"
        style={{
          display: 'grid',
          gridTemplateColumns: `repeat(${BOARD_SIZE}, 20px)`,
          gridTemplateRows: `repeat(${BOARD_SIZE}, 20px)`,
          gap: '1px',
          backgroundColor: '#333',
        }}
      >
        {renderBoard()}
      </div>

      {gameOver && (
        <div className="game-over">
          <h2>ゲームオーバー</h2>
          <p>最終スコア: {score}</p>
          <button onClick={resetGame}>もう一度プレイ</button>
        </div>
      )}

      <div className="instructions">
        <p>矢印キーで操作してください</p>
        <p>食べ物を食べるとスコアが上がり、スピードも速くなります</p>
      </div>
    </div>
  );
}

export default SnakeGame;

このスネークゲームでは、useEffectでゲームループを実装しています。 setIntervalで定期的にゲーム状態を更新し、衝突判定やスコア管理を行っています。

useCallbackを使ってキーボード操作を最適化し、window.addEventListenerでイベントリスナーを設定しています。

CSS Grid Layoutを使ってゲームボードを描画し、各セルの状態に応じてクラスを切り替えています。

学習リソースと次のステップ

Reactの可能性を最大限に活用するための学習方法をご紹介します。

分野別学習ロードマップ

Web開発特化

  1. 基礎: React + HTML/CSS + JavaScript
  2. 状態管理: Redux または Zustand
  3. ルーティング: React Router
  4. UI: Material-UI または Chakra UI
  5. テスト: Jest + React Testing Library
  6. パフォーマンス: React.memo、useMemo、useCallback

フルスタック開発

  1. フロントエンド: React + TypeScript
  2. バックエンド: Node.js + Express
  3. データベース: MongoDB または PostgreSQL
  4. 認証: NextAuth.js または Firebase Auth
  5. デプロイ: Vercel または Netlify

モバイルアプリ開発

  1. React Native基礎: コンポーネント、ナビゲーション
  2. ネイティブ機能: カメラ、位置情報、プッシュ通知
  3. 状態管理: Redux Toolkit
  4. テスト: Detox
  5. 配布: App Store、Google Play Store

実践的なプロジェクトアイデア

初級レベル

  • Todoアプリ
  • 天気予報アプリ
  • 電卓アプリ
  • タイマーアプリ

中級レベル

  • ブログアプリ
  • ECサイト
  • チャットアプリ
  • 家計簿アプリ

上級レベル

  • SNSプラットフォーム
  • プロジェクト管理ツール
  • 動画配信プラットフォーム
  • リアルタイム協業ツール

まとめ

Reactでできることを幅広く解説しました。

Web開発の可能性

SPA、PWA、ダッシュボードなど、あらゆるWebアプリケーションを構築できます。 フォーム管理、データ取得、状態管理まで、すべてReactで完結します。

クロスプラットフォーム開発

React Nativeでモバイルアプリ、Electronでデスクトップアプリまでカバーできます。 一つの技術で、複数のプラットフォームに対応できるのは大きな魅力です。

モダンな開発体験

Next.jsによるSSR、TypeScriptとの組み合わせで、企業レベルの開発が可能です。 パフォーマンス最適化や SEO対策も、Reactエコシステムで解決できます。

新しい分野への展開

VR/AR、ゲーム開発など、従来の枠を超えた活用も広がっています。 Reactの概念を応用して、様々な分野でイノベーションを起こせます。

継続的な進化

React エコシステムは日々進化しており、新しい可能性が続々と生まれています。 コミュニティも活発で、常に最新の技術を学べる環境があります。

Reactは単なるライブラリではなく、モダンなデジタル体験を創造するためのプラットフォームです。

基礎をしっかり学習して、自分の興味のある分野でReactの可能性を探求してみてください。

きっと、想像を超える素晴らしいアプリケーションを作ることができるはずです。

関連記事