Initializing Enclave...

How to Fix 'Cannot Read Properties of Undefined (Reading map)' in React Strict Mode

Threat/Impact Level: HIGH | Downtime Risk: HIGH | Time to Fix: 10 mins

TL;DR

  • What broke: A component calls .map() on a variable that is undefined at first render. React 18 Strict Mode's deliberate double-invocation of render functions surfaces this race condition that a single render would silently mask.
  • How to fix it: Initialize all array state to [], not undefined or null. Guard every .map() call with optional chaining or a nullish coalescing fallback.
  • Shortcut: Use our Client-Side Sandbox below to auto-refactor this — paste your component and get patched code without sending your data anywhere.

The Incident (What Does the Error Mean?)

Raw browser console output:

Uncaught TypeError: Cannot read properties of undefined (reading 'map')
    at ProductList (ProductList.jsx:14:22)
    at renderWithHooks (react-dom.development.js:14985)
    at mountIndeterminateComponent (react-dom.development.js:17811)
    at beginWork (react-dom.development.js:19049)

Immediate consequence: The entire React subtree below this component unmounts. Users see a blank screen or the nearest <ErrorBoundary> fallback. If no ErrorBoundary exists, the whole app goes white. This is a full user-facing outage for any route rendering this component.

React 18 Strict Mode intentionally runs useState initializers and the render function twice in development. On the first pass, async data hasn't resolved. If your state was initialized as undefined instead of [], .map() detonates on pass one before the second pass or useEffect can populate it.


The Attack Vector / Blast Radius

This isn't just a dev-mode nuisance. The double-render in Strict Mode is a canary — it means your component has no defensive initialization, which means the same crash will occur in production under any of these real conditions:

  • Slow network / API timeout: useEffect fetch hasn't resolved before first paint.
  • API returns null or omits the array key: Backend schema drift silently breaks the frontend.
  • React Suspense boundaries: If a parent suspends and re-mounts the child, the child re-initializes from scratch — hitting the same undefined path.
  • SSR hydration mismatch (Next.js / Remix): Server renders with empty data, client attempts to hydrate with undefined state, triggering a hydration error cascade on top of the TypeError.

Blast radius: Every component in the subtree fails to render. If this component is in a shared layout (sidebar, nav, dashboard shell), the entire application surface goes down, not just one widget.


How to Fix It

Basic Fix — Defensive Initialization

The root issue is always the same: state or props holding an array were never given a safe default.

- const [products, setProducts] = useState();
+ const [products, setProducts] = useState([]);

  useEffect(() => {
    fetch('/api/products')
      .then(res => res.json())
-     .then(data => setProducts(data));
+     .then(data => setProducts(data?.products ?? []));
  }, []);

  return (
    <ul>
-     {products.map(p => <li key={p.id}>{p.name}</li>)}
+     {products.map(p => <li key={p.id}>{p.name}</li>)}
    </ul>
  );

Initializing to [] is sufficient when you own the state. The ?.products ?? [] guard on the setter handles backend schema drift.

Enterprise Best Practice — Layered Defense with Loading and Error States

In production systems, a bare useState([]) is the floor, not the ceiling. You need explicit loading and error states to prevent rendering stale or partial data.

- const [products, setProducts] = useState();
- const [loading, setLoading] = useState(false);
+ const [products, setProducts] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);

  useEffect(() => {
-   fetch('/api/products')
-     .then(res => res.json())
-     .then(data => setProducts(data));
+   let cancelled = false;
+   setLoading(true);
+   fetch('/api/products')
+     .then(res => {
+       if (!res.ok) throw new Error(`HTTP ${res.status}`);
+       return res.json();
+     })
+     .then(data => {
+       if (!cancelled) setProducts(Array.isArray(data?.products) ? data.products : []);
+     })
+     .catch(err => { if (!cancelled) setError(err.message); })
+     .finally(() => { if (!cancelled) setLoading(false); });
+   return () => { cancelled = true; };
  }, []);

+ if (loading) return <Spinner />;
+ if (error) return <ErrorMessage message={error} />;

  return (
    <ul>
-     {products.map(p => <li key={p.id}>{p.name}</li>)}
+     {(products ?? []).map(p => <li key={p.id}>{p.name}</li>)}
    </ul>
  );

Key additions:

  • Cleanup flag (cancelled): Prevents setState on an unmounted component — the second Strict Mode unmount/remount cycle will trigger this without the guard.
  • Array.isArray() check: Validates the API response shape before trusting it.
  • .finally() loading reset: Ensures loading clears even on error, preventing infinite spinners.
  • Double guard (products ?? []).map: Last-line defense if a parent passes a bad prop.

💡 Tired of pasting proprietary configs into ChatGPT? Generic AI tools log your component code, API endpoint paths, and internal data schemas. StackEngine is a zero-backend, pure Client-Side WASM utility. Drop your failing component into the sandbox above. We redact your secrets locally in the browser and auto-generate the refactored code using your own API key.


Prevention in CI/CD

This class of bug should never reach a PR review, let alone production. Automate it out:

1. ESLint Rules (immediate, zero-cost)

Add to .eslintrc:

{
  "rules": {
    "@typescript-eslint/no-unsafe-call": "error",
    "@typescript-eslint/no-unsafe-member-access": "error"
  }
}

With TypeScript strict mode ("strict": true in tsconfig.json), the compiler will flag undefined.map at build time before any browser ever runs it.

2. TypeScript Strict Null Checks

{
  "compilerOptions": {
    "strict": true,
    "strictNullChecks": true
  }
}

This forces every array type to be T[] not T[] | undefined, making the bad initialization a compile error.

3. React Testing Library — Strict Mode Test Harness

Wrap all component tests in <StrictMode> explicitly so CI catches double-render crashes before merge:

import { StrictMode } from 'react';
import { render } from '@testing-library/react';

const strictRender = (ui) => render(<StrictMode>{ui}</StrictMode>);

Add a baseline test asserting the component renders without throwing when given no data.

4. Sentry / Error Monitoring — Alert on TypeError

Add a Sentry beforeSend filter that escalates any TypeError: Cannot read properties of undefined to P1 with full component stack trace. Don't wait for user reports.

Related Diagnostics

"Part of the Syntax Utility Matrix."

View all 153 Syntax Tools →