0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

はじめに

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

目標

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

Reactのパフォーマンス最適化

1. Lazy Loadingの導入

ProductListを遅延ロードします。App.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を新規作成:

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の使用

ProductItemReact.memoでラップし、無駄な再レンダリングを防ぎます。

商品検索機能の追加

バックエンドの更新

backend/index.jsに検索エンドポイントを追加:

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に検索機能を追加:

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にエラーミドルウェアを追加:

app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({ message: 'サーバーエラーが発生しました' });
});

動作確認

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

次回予告

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


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

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?