Reactでできること一覧 - モダンWeb開発の可能性を解説
Reactで構築できるアプリケーションを網羅的に解説。Webアプリ、モバイルアプリ、デスクトップアプリまで、Reactの無限の可能性を紹介
みなさん、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
コンポーネントがHeader
、MainContent
、Footer
を組み合わせて作られています。
このコンポーネントベースの設計により、どのプラットフォームでも同じ考え方で開発できます。
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
の代わりにView
、p
の代わりに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と似ていますが、flexDirection
やalignItems
などの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.share
APIを使ってネイティブシェア機能を実装しています。
対応していないブラウザでは、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開発特化
- 基礎: React + HTML/CSS + JavaScript
- 状態管理: Redux または Zustand
- ルーティング: React Router
- UI: Material-UI または Chakra UI
- テスト: Jest + React Testing Library
- パフォーマンス: React.memo、useMemo、useCallback
フルスタック開発
- フロントエンド: React + TypeScript
- バックエンド: Node.js + Express
- データベース: MongoDB または PostgreSQL
- 認証: NextAuth.js または Firebase Auth
- デプロイ: Vercel または Netlify
モバイルアプリ開発
- React Native基礎: コンポーネント、ナビゲーション
- ネイティブ機能: カメラ、位置情報、プッシュ通知
- 状態管理: Redux Toolkit
- テスト: Detox
- 配布: 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の可能性を探求してみてください。
きっと、想像を超える素晴らしいアプリケーションを作ることができるはずです。