Reactとバックエンドで作る！実践Web開発入門 | 第9章: パフォーマンス最適化と機能追加

## はじめに
第8章でアプリをデプロイしました。今回は、パフォーマンスを最適化し、検索機能を追加してユーザー体験を向上させます。Reactとバックエンドの両方を改良します。

## 目標
- Reactのパフォーマンスを最適化する  
- 商品検索機能を追加する  
- エラーハンドリングを強化する  

## Reactのパフォーマンス最適化
### 1. Lazy Loadingの導入
`ProductList`を遅延ロードします。`App.tsx`を更新：
```tsx
import React, { Suspense } from 'react';
import './App.css';
import { ProductProvider } from './ProductContext';
import { AuthProvider } from './AuthContext';

const ProductList = React.lazy(() => import('./ProductList'));

function App() {
  return (
    <AuthProvider>
      <ProductProvider>
        <div className="App">
          <Header />
          <Suspense fallback={<main>読み込み中...</main>}>
            <ProductList />
          </Suspense>
          <Footer />
        </div>
      </ProductProvider>
    </AuthProvider>
  );
}

const Header = () => {
  const { token, logout } = useAuthContext();
  return (
    <header>
      <h1>商品リスト</h1>
      {token && <button onClick={logout}>ログアウト</button>}
    </header>
  );
};

const Footer = () => (
  <footer>
    <p>© 2025 React Web開発入門</p>
  </footer>
);

export default App;
```
`ProductList.tsx`を新規作成：
```tsx
import React, { useEffect } from 'react';
import { useProductContext } from './ProductContext';
import { useAuthContext } from './AuthContext';

export default function ProductList() {
  const { products, setProducts } = useProductContext();
  const { token, login } = useAuthContext();
  const [loading, setLoading] = React.useState(true);
  const [error, setError] = React.useState(null);
  const [newProduct, setNewProduct] = React.useState({ name: '', price: '' });
  const [credentials, setCredentials] = React.useState({ username: '', password: '' });

  useEffect(() => {
    const fetchProducts = async () => {
      try {
        const response = await fetch(`${process.env.REACT_APP_API_URL}/products`);
        if (!response.ok) throw new Error('データの取得に失敗しました');
        const data = await response.json();
        setProducts(data);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };
    fetchProducts();
  }, [setProducts]);

  const handleAddProduct = async (e) => {
    e.preventDefault();
    if (!token) return alert('ログインしてください');
    try {
      const response = await fetch(`${process.env.REACT_APP_API_URL}/products`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${token}`,
        },
        body: JSON.stringify({ name: newProduct.name, price: Number(newProduct.price) }),
      });
      if (!response.ok) throw new Error('商品の追加に失敗しました');
      const addedProduct = await response.json();
      setProducts((prev) => [...prev, addedProduct]);
      setNewProduct({ name: '', price: '' });
    } catch (err) {
      setError(err.message);
    }
  };

  const handleLogin = async (e) => {
    e.preventDefault();
    try {
      const response = await fetch(`${process.env.REACT_APP_API_URL}/login`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(credentials),
      });
      if (!response.ok) throw new Error('ログインに失敗しました');
      const { token } = await response.json();
      login(token);
    } catch (err) {
      setError(err.message);
    }
  };

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

  return (
    <main>
      {!token ? (
        <form onSubmit={handleLogin}>
          <h2>ログイン</h2>
          <input
            type="text"
            placeholder="ユーザー名"
            value={credentials.username}
            onChange={(e) => setCredentials({ ...credentials, username: e.target.value })}
          />
          <input
            type="password"
            placeholder="パスワード"
            value={credentials.password}
            onChange={(e) => setCredentials({ ...credentials, password: e.target.value })}
          />
          <button type="submit">ログイン</button>
        </form>
      ) : (
        <>
          <h2>商品一覧</h2>
          <form onSubmit={handleAddProduct}>
            <input
              type="text"
              placeholder="商品名"
              value={newProduct.name}
              onChange={(e) => setNewProduct({ ...newProduct, name: e.target.value })}
            />
            <input
              type="number"
              placeholder="価格"
              value={newProduct.price}
              onChange={(e) => setNewProduct({ ...newProduct, price: e.target.value })}
            />
            <button type="submit">追加</button>
          </form>
          <ul>
            {products.map((product) => (
              <ProductItem key={product._id} name={product.name} price={product.price} />
            ))}
          </ul>
        </>
      )}
    </main>
  );
}

const ProductItem = React.memo(({ name, price }) => (
  <li>
    {name} - {price}円
  </li>
));
```

### 2. React.memoの使用
`ProductItem`を`React.memo`でラップし、無駄な再レンダリングを防ぎます。

## 商品検索機能の追加
### バックエンドの更新
`backend/index.js`に検索エンドポイントを追加：
```javascript
app.get('/products/search', async (req, res) => {
  const { q } = req.query;
  const products = await Product.find({ name: { $regex: q, $options: 'i' } });
  res.json(products);
});
```

### フロントエンドの更新
`ProductList.tsx`に検索機能を追加：
```tsx
export default function ProductList() {
  const { products, setProducts } = useProductContext();
  const { token, login } = useAuthContext();
  const [loading, setLoading] = React.useState(true);
  const [error, setError] = React.useState(null);
  const [newProduct, setNewProduct] = React.useState({ name: '', price: '' });
  const [credentials, setCredentials] = React.useState({ username: '', password: '' });
  const [searchQuery, setSearchQuery] = React.useState('');

  useEffect(() => {
    const fetchProducts = async () => {
      try {
        const url = searchQuery
          ? `${process.env.REACT_APP_API_URL}/products/search?q=${searchQuery}`
          : `${process.env.REACT_APP_API_URL}/products`;
        const response = await fetch(url);
        if (!response.ok) throw new Error('データの取得に失敗しました');
        const data = await response.json();
        setProducts(data);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };
    fetchProducts();
  }, [setProducts, searchQuery]);

  const handleAddProduct = async (e) => {
    e.preventDefault();
    if (!token) return alert('ログインしてください');
    try {
      const response = await fetch(`${process.env.REACT_APP_API_URL}/products`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${token}`,
        },
        body: JSON.stringify({ name: newProduct.name, price: Number(newProduct.price) }),
      });
      if (!response.ok) throw new Error('商品の追加に失敗しました');
      const addedProduct = await response.json();
      setProducts((prev) => [...prev, addedProduct]);
      setNewProduct({ name: '', price: '' });
    } catch (err) {
      setError(err.message);
    }
  };

  const handleLogin = async (e) => {
    e.preventDefault();
    try {
      const response = await fetch(`${process.env.REACT_APP_API_URL}/login`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(credentials),
      });
      if (!response.ok) throw new Error('ログインに失敗しました');
      const { token } = await response.json();
      login(token);
    } catch (err) {
      setError(err.message);
    }
  };

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

  return (
    <main>
      {!token ? (
        <form onSubmit={handleLogin}>
          <h2>ログイン</h2>
          <input
            type="text"
            placeholder="ユーザー名"
            value={credentials.username}
            onChange={(e) => setCredentials({ ...credentials, username: e.target.value })}
          />
          <input
            type="password"
            placeholder="パスワード"
            value={credentials.password}
            onChange={(e) => setCredentials({ abuso...credentials, password: e.target.value })}
          />
          <button type="submit">ログイン</button>
        </form>
      ) : (
        <>
          <h2>商品一覧</h2>
          <input
            type="text"
            placeholder="商品を検索..."
            value={searchQuery}
            onChange={(e) => setSearchQuery(e.target.value)}
          />
          <form onSubmit={handleAddProduct}>
            <input
              type="text"
              placeholder="商品名"
              value={newProduct.name}
              onChange={(e) => setNewProduct({ ...newProduct, name: e.target.value })}
            />
            <input
              type="number"
              placeholder="価格"
              value={newProduct.price}
              onChange={(e) => setNewProduct({ ...newProduct, price: e.target.value })}
            />
            <button type="submit">追加</button>
          </form>
          <ul>
            {products.map((product) => (
              <ProductItem key={product._id} name={product.name} price={product.price} />
            ))}
          </ul>
        </>
      )}
    </main>
  );
}
```

## エラーハンドリングの強化
`backend/index.js`にエラーミドルウェアを追加：
```javascript
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({ message: 'サーバーエラーが発生しました' });
});
```

## 動作確認
1. バックエンドを再デプロイ。
2. フロントエンドをビルドし、再デプロイ。
3. 検索機能と商品追加をテスト。

## 次回予告
第10章でシリーズを総括し、次のステップを提案します。お楽しみに！

---

この記事が役に立ったら、ぜひ「いいね」や「ストック」をお願いします！質問や感想はコメント欄で気軽にどうぞ。次回も一緒に学びましょう！