はじめに
第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の使用
ProductItem
をReact.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: 'サーバーエラーが発生しました' });
});
動作確認
- バックエンドを再デプロイ。
- フロントエンドをビルドし、再デプロイ。
- 検索機能と商品追加をテスト。
次回予告
第10章でシリーズを総括し、次のステップを提案します。お楽しみに!
この記事が役に立ったら、ぜひ「いいね」や「ストック」をお願いします!質問や感想はコメント欄で気軽にどうぞ。次回も一緒に学びましょう!