要約
undefined に対して .map() を呼び出しています。フォールバックを追加するだけで解決します:
// クラッシュする
{items.map(item => <li key={item.id}>{item.name}</li>)}
// 安全
{(items ?? []).map(item => <li key={item.id}>{item.name}</li>)}
エラーの全文
TypeError: Cannot read properties of undefined (reading 'map')
at ProductList (ProductList.jsx:12)
at renderWithHooks (react-dom.development.js:14985)
React はコンポーネントを同期的にレンダリングします。最初のレンダリング時に products が undefined だと — ほんの一瞬であっても — .map() の呼び出しは即座に失敗します。猶予はありません。
原因
1. ステートが空配列ではなく undefined で初期化されている
// 誤り — 引数なしの useState() はデフォルトで undefined になる
const [products, setProducts] = useState();
// 正しい
const [products, setProducts] = useState([]);
2. APIデータがまだ読み込まれていない
useEffect は最初のレンダリングの後に実行されます。そのため、初回描画時のステートは初期化した値のまま — つまり何もない状態かもしれません。
useEffect(() => {
fetch('/api/products')
.then(res => res.json())
.then(data => setProducts(data)); // 最初のレンダリングの後に届く
}, []);
3. 親コンポーネントと子コンポーネントでプロップ名が異なる
// 親は 'items' として渡している
<ProductList items={products} />
// 子は 'data' として読んでいる — これは undefined になる
function ProductList({ data }) {
return data.map(...); // クラッシュ
}
4. APIレスポンスの構造が想定と異なる
// 想定: [{ id: 1, name: 'Widget' }, ...]
// 実際のAPIレスポンス: { data: [...], total: 42 }
setProducts(response); // products にオブジェクトがセットされ、配列にならない
これは多くの人がはまるポイントです。構造を決めつける前に、必ず生のレスポンスをログに出力しましょう。
修正手順
修正1:ステートを空配列で初期化する
最もよくある原因です。配列のステートを undefined のままにしてはいけません。
const [products, setProducts] = useState([]); // ✅
修正2:マッピング前にフォールバックを追加する
どこまで防御的に書くかに応じて、3つの選択肢があります:
// オプションA: Null合体演算子(undefined/null に対して最もシンプル)
{(products ?? []).map(product => (
<li key={product.id}>{product.name}</li>
))}
// オプションB: オプショナルチェーン(undefined の場合は何も描画しない)
{products?.map(product => (
<li key={product.id}>{product.name}</li>
))}
// オプションC: Array.isArray(最も明示的)
{Array.isArray(products) && products.map(product => (
<li key={product.id}>{product.name}</li>
))}
値がオブジェクト、null、または数値になる可能性がある場合(単に undefined だけでなく)は、Array.isArray() を使いましょう。3つの中で最も堅牢な方法です。
修正3:非同期データのローディング状態を管理する
function ProductList() {
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('/api/products')
.then(res => res.json())
.then(data => {
setProducts(data);
setLoading(false);
});
}, []);
if (loading) return <p>読み込み中...</p>;
return (
<ul>
{products.map(product => (
<li key={product.id}>{product.name}</li>
))}
</ul>
);
}
修正4:APIレスポンスを使用前に確認する
fetch('/api/products')
.then(res => res.json())
.then(data => {
console.log(data); // 構造を決めつける前に必ずログを確認する
// APIが { data: [...], total: 42 } 形式で返す場合
setProducts(data.data ?? data);
});
修正5:開発時にプロップの型を強制する
誤ったプロップ名を、混乱したバグ報告が届く前に — 本番環境に到達する前に — キャッチしましょう。
// PropTypes
import PropTypes from 'prop-types';
ProductList.propTypes = {
items: PropTypes.array.isRequired,
};
ProductList.defaultProps = {
items: [],
};
// TypeScript — コンパイル時の強制、実行時コストなし
interface ProductListProps {
items: Product[];
}
function ProductList({ items }: ProductListProps) {
return (
<ul>
{items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
デバッグチェックリスト
- DevTools を開いてスタックトレースを確認する — クラッシュが発生した正確なファイルと行番号が表示されます
.map()の直前にconsole.log(yourVariable)を追加して、最初のレンダリング時に実際にどんな値が入っているか確認するuseStateの呼び出しを確認する — 初期値はundefinedですか、それとも[]ですか?- 親コンポーネントで渡しているプロップ名と、子コンポーネントで分割代入しているプロップ名を比較する — タイポ1つで十分クラッシュします
setStateを呼び出す前に生のAPIレスポンスをログに出力して、配列が想定した場所にあることを確認する
修正の確認
修正を適用した後、4つの点を確認してください:
- ブラウザのコンソールを開く —
TypeErrorが消えているはずです。 - 実際のデータでリストが正しく表示される。
- DevTools の Network タブで Slow 3G にスロットリングして非同期パスをストレステストする。
- 空配列レスポンスでテストする — クラッシュせず、空のリストが表示されるだけであることを確認。
クイックリファレンス
| 原因 | 修正 |
|-----------------------------------|-----------------------------------------------|
| 初期値なしの useState() | useState([]) |
| 最初のレンダリング時の非同期データ | ローディング状態 + 条件付きレンダリング |
| 誤ったプロップ名 | 親と子でプロップ名を一致させる |
| APIが配列でなくオブジェクトを返す | setProducts(response.data ?? response) |
| 親からの null になりうるプロップ | defaultProps または ?? [] フォールバック |

