POS開発 フロントエンド チートコード — 非同期
7. 非同期処理
コールバック → Promise → async/await の流れ
JavaScriptはシングルスレッドのため、時間のかかる処理(API呼び出し、タイマーなど)は非同期で実行される。async/awaitが最も読みやすく推奨。
setTimeout / setInterval
// 一定時間後に1回実行 const timerId = setTimeout(() => { console.log("3秒後に実行"); }, 3000); clearTimeout(timerId); // キャンセル // 一定間隔で繰り返し実行 const intervalId = setInterval(() => { console.log("5秒ごとに実行"); }, 5000); clearInterval(intervalId); // 停止
Promise 基本
// Promiseの作成 const myPromise = new Promise((resolve, reject) => { const success = true; if (success) { resolve("成功データ"); // 成功時 } else { reject(new Error("エラー")); // 失敗時 } }); // then / catch / finally チェーン myPromise .then(data => { console.log(data); // "成功データ" return processData(data); // 次のthenに値を渡す }) .then(result => { console.log(result); // processDataの結果 }) .catch(err => { console.error(err); // どこかでエラーが起きたら }) .finally(() => { console.log("完了"); // 成功でも失敗でも実行 });
async / await
// async関数: Promiseを返す関数を宣言 async function loadProducts() { const response = await fetch('/api/products'); // Promiseの完了を待つ const data = await response.json(); // これもPromise return data; } // アロー関数版 const loadProducts = async () => { const res = await fetch('/api/products'); return res.json(); }; // 呼び出し側 const products = await loadProducts(); // トップレベルawait(モジュール内) // または loadProducts().then(products => { renderProducts(products); });
// React: useEffect コールバックは async にしない(Promise返すとcleanup扱いになる) // 中に async IIFE を書くのが定番パターン function ProductList() { const [products, setProducts] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { let cancelled = false; (async () => { // ← 即時実行 async 関数 try { const res = await fetch('/api/products'); const data = await res.json(); if (!cancelled) setProducts(data); } finally { if (!cancelled) setLoading(false); } })(); return () => { cancelled = true; }; // unmount 時の二重更新を防ぐ }, []); if (loading) return <p>読込中...</p>; return <ul>{products.map(p => <li key={p.id}>{p.name}</li>)}</ul>; }
エラーハンドリング
async function checkout(cartItems) { const loading = showLoading(); try { const result = await submitOrder(cartItems); showSuccess("注文が完了しました"); return result; } catch (error) { // エラーの種類で分岐 if (error instanceof TypeError) { showError("ネットワークエラー: サーバーに接続できません"); } else if (error.status === 400) { showError("入力内容に問題があります"); } else { showError(`予期しないエラー: ${error.message}`); } } finally { hideLoading(loading); // 成功でも失敗でも実行される } }
Promise.all / Promise.allSettled / Promise.race
// Promise.all: 全て成功するまで待つ(1つでも失敗したら即catch) try { const [users, products, orders] = await Promise.all([ fetchUsers(), fetchProducts(), fetchOrders(), ]); } catch (e) { // いずれか1つが失敗した場合 } // Promise.allSettled: 全ての結果を取得(失敗しても止まらない) const results = await Promise.allSettled([ fetchUsers(), fetchProducts(), ]); results.forEach(r => { if (r.status === "fulfilled") console.log(r.value); if (r.status === "rejected") console.error(r.reason); }); // Promise.race: 最初に完了(成功or失敗)したものを採用 const result = await Promise.race([ fetchData(), new Promise((_, reject) => setTimeout(() => reject(new Error("タイムアウト")), 5000) ), ]);
非同期の逐次実行 vs 並列実行
// ❌ 逐次実行(遅い: A完了 → B完了 → C完了) const a = await fetchA(); const b = await fetchB(); const c = await fetchC(); // ✅ 並列実行(速い: A,B,Cを同時に開始) const [a, b, c] = await Promise.all([ fetchA(), fetchB(), fetchC(), ]); // 配列の各要素を順番に非同期処理する場合 for (const item of items) { await processItem(item); // 1つずつ順番に }
8. Fetch API
GETリクエスト
// 基本的なGET const response = await fetch('/api/products'); const data = await response.json(); // クエリパラメータ付き const params = new URLSearchParams({ category: "drink", limit: "10", page: "1", }); const res = await fetch(`/api/products?${params}`); const drinks = await res.json();
// React: 依存配列で「いつ再取得するか」を宣言する function ProductsByCategory({ category }) { const [data, setData] = useState([]); const [error, setError] = useState(null); useEffect(() => { const params = new URLSearchParams({ category, limit: '10' }); fetch(`/api/products?${params}`) .then(r => r.json()) .then(setData) .catch(setError); }, [category]); // ← category が変わるたび再取得 if (error) return <p>エラー: {error.message}</p>; return ( <ul> {data.map(p => <li key={p.id}>{p.name} ¥{p.price}</li>)} </ul> ); }
POSTリクエスト(JSON送信)
const response = await fetch('/api/orders', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ items: cartItems, paymentMethod: "cash", total: 2500, }), }); const result = await response.json();
PUT / DELETE
// PUT: 更新 await fetch(`/api/products/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ price: newPrice }), }); // DELETE: 削除 await fetch(`/api/products/${id}`, { method: 'DELETE', });
レスポンスの処理
const response = await fetch(url); // ステータス確認 response.ok; // true (200-299の場合) response.status; // 200, 404, 500 など response.statusText; // "OK", "Not Found" など // ボディの取得(※1回しか読めない) const json = await response.json(); // JSONとしてパース const text = await response.text(); // テキストとして取得 const blob = await response.blob(); // バイナリ(画像など) // レスポンスヘッダーの確認 response.headers.get('Content-Type');
エラーハンドリング
fetchは404や500でもrejectしない(ネットワークエラーのみreject)。response.okのチェックが必須。
async function apiFetch(url, options = {}) { try { const response = await fetch(url, options); if (!response.ok) { // HTTPエラー(404, 500など)→ fetchはrejectしないので自分でthrow const errorBody = await response.text(); throw new Error(`HTTP ${response.status}: ${errorBody}`); } return await response.json(); } catch (error) { if (error instanceof TypeError) { // ネットワークエラー(サーバー未到達、DNS解決失敗など) console.error("ネットワークエラー:", error.message); } else { // HTTPエラーまたはJSONパースエラー console.error("APIエラー:", error.message); } throw error; } }
FormDataで送信
// フォームから直接取得して送信 const form = document.getElementById('myForm'); const formData = new FormData(form); // Content-Typeは自動設定されるので指定不要 await fetch('/api/upload', { method: 'POST', body: formData, }); // FormDataの中身を確認 for (const [key, value] of formData) { console.log(`${key}: ${value}`); } // FormDataをオブジェクトに変換 const obj = Object.fromEntries(formData);
AbortController(リクエストのキャンセル)
const controller = new AbortController(); // タイムアウト付きfetch const timeoutId = setTimeout(() => controller.abort(), 5000); try { const res = await fetch('/api/data', { signal: controller.signal, }); clearTimeout(timeoutId); return await res.json(); } catch (e) { if (e.name === 'AbortError') { console.log('リクエストがキャンセルされました'); } } // 検索入力時の前回リクエストキャンセル let currentController = null; input.addEventListener('input', async (e) => { currentController?.abort(); // 前回のリクエストをキャンセル currentController = new AbortController(); try { const res = await fetch(`/api/search?q=${e.target.value}`, { signal: currentController.signal, }); const data = await res.json(); renderResults(data); } catch (e) { if (e.name !== 'AbortError') throw e; } });
// React: useEffect の cleanup 関数で abort するのが典型パターン // query が変わると前回の fetch が自動キャンセルされる function SearchResults({ query }) { const [results, setResults] = useState([]); useEffect(() => { if (!query) { setResults([]); return; } const controller = new AbortController(); (async () => { try { const res = await fetch(`/api/search?q=${query}`, { signal: controller.signal, }); setResults(await res.json()); } catch (e) { if (e.name !== 'AbortError') throw e; } })(); return () => controller.abort(); // ← 次の effect 実行前に自動発火 }, [query]); return <ul>{results.map(r => <li key={r.id}>{r.name}</li>)}</ul>; }