Fix: React TypeError: Cannot read property 'map' of undefined
Part of: React & Frontend Errors
Quick Answer
How to fix React TypeError Cannot read property map of undefined caused by uninitialized state, async data loading, wrong API response structure, and missing default values.
Cannot read properties of undefined (reading ‘map’)
I hit this constantly when I was newer to React, and it was almost always the same thing: the component rendered once before my fetch came back, so .map() ran on the undefined I had started the state with. The error text makes it sound like a deep bug, but it is really a race between React’s synchronous first render and your asynchronous data. I learned to fix it at the source by initializing list state to [] rather than guarding every .map() after the fact. In my experience the only time that is not enough is when the API hands back an envelope like { data: [...] } and you store the whole object by mistake, then the value is defined but still has no .map.
Your React app crashes with:
TypeError: Cannot read property 'map' of undefinedOr the modern equivalent:
TypeError: Cannot read properties of undefined (reading 'map')You called .map() on a value that is undefined instead of an array. This typically happens when rendering a list from state or props that has not been initialized or loaded yet.
Rendering Before the Data Arrives
.map() is an array method. When you call someVar.map(...), JavaScript first evaluates someVar. If it is undefined (or null), accessing .map on it throws a TypeError.
The crash is almost always a timing problem: the component renders once before the data arrives. React’s render cycle is synchronous, but the network request that fills your state is asynchronous. The very first render runs with whatever the initial state was. If that initial state was undefined, the .map() call in JSX runs before any data exists. The fix is either to make the initial state safe (an empty array) or to guard the render with a conditional check.
The second common shape is a shape mismatch: the response arrived but it is not the structure you assumed. REST APIs frequently wrap arrays inside an envelope ({ data: [...] }, { results: [...] }, { items: [...] }, { data: { users: [...] } }). If you set state to the envelope and then call .map on it, the envelope is an object, and an object has no .map. The error message reads the same way in both cases, so always log the actual response shape during debugging.
In React, this commonly occurs because:
- State initialized without a default array.
useState()without an initial value defaults toundefined. - Data not loaded yet. The component renders before the API call completes.
- API response structure changed. The data is nested differently than expected.
- Prop not passed. A parent component did not pass the expected array prop.
- Conditional data. The data exists in some cases but not others.
Platform and Environment Differences
The same .map of undefined crash surfaces in noticeably different ways depending on where your React code runs.
SSR (Next.js, Remix) vs CSR. In a pure client-side React app, the first render always runs in the browser, so a null initial state plus a guard works fine. With Next.js App Router or Remix, the component first renders on the server during the request. If the server fetch returns undefined (or your loader returns the wrong key), the error throws during SSR and you get a 500 response, not a runtime crash in DevTools. Next.js wraps it as “An error occurred in the Server Component” with the underlying Cannot read properties of undefined in the server logs. Always read the server log when SSR is involved, the browser only sees the rendered error page.
Browser-only globals during build. Code that reads window.something.items or localStorage.list runs on the server during SSR/SSG and crashes with Cannot read properties of undefined. Wrap browser-only access in typeof window !== "undefined" checks or move the access into useEffect. The hydration mismatch errors that show up next come from the same root cause.
Node vs Bun when running React tests. Jest under Node sets globalThis.window only when you configure jsdom as the test environment. Under the default node environment, a bare window.someState throws ReferenceError: window is not defined, while the defensive form globalThis.window?.someState quietly evaluates to undefined, and the .map further down then throws this article’s TypeError. Which of those two errors you see tells you how the code reached for the global. Vitest behaves the same way but uses environment: "jsdom" in its config. Bun’s bun test ships no DOM at all by default, you register happy-dom yourself (via @happy-dom/global-registrator), and happy-dom differs subtly from jsdom. So the same component can pass under Jest with jsdom and throw under Bun with happy-dom, because one provides a window global your code leans on and the other does not.
Mobile webviews. Older iOS Safari and Android WebView builds lag desktop browsers on syntax like optional chaining and nullish coalescing, so an untranspiled data?.users ?? [] can throw a syntax error instead of guarding, make sure your build actually targets those engines. The more common trap is WKWebView’s different cookie and CORS behavior under React Native: a fetch that works on desktop can fail in the webview, the .then that sets state never runs, and the component renders with its initial undefined and crashes. (res.json() itself does not vary by browser, it parses standard JSON regardless of content type, and rejects rather than returning undefined on bad input, so a “parsed to undefined” theory is a red herring.)
React Server Components. With RSC, the component runs on the server and streams to the client. An undefined result from a server-side fetch crashes at the server boundary. You will not see this error in the browser console, it appears in the server logs and the affected component tree falls back to the nearest error boundary or root error page.
Fix 1: Initialize State with an Empty Array
The most common fix. Give your state a default empty array:
Broken:
function UserList() {
const [users, setUsers] = useState(); // undefined!
return (
<ul>
{users.map(user => <li key={user.id}>{user.name}</li>)}
</ul>
);
}Fixed:
function UserList() {
const [users, setUsers] = useState([]); // Empty array — .map() works
return (
<ul>
{users.map(user => <li key={user.id}>{user.name}</li>)}
</ul>
);
}useState([]) starts with an empty array. .map() on an empty array returns an empty array, no error, no rendered items. When data loads, the state updates and the list renders.
The habit that prevents most of these is matching the state’s initial value to its eventual type. If it will hold an array, start it at []; if an object, start it at {} or at null and then handle the null branch explicitly. The initial value is the one render you control completely, so make it safe.
Fix 2: Add a Loading Guard
Check if data exists before rendering:
function UserList() {
const [users, setUsers] = useState(null);
useEffect(() => {
fetch("/api/users")
.then(res => res.json())
.then(setUsers);
}, []);
if (!users) return <p>Loading...</p>;
return (
<ul>
{users.map(user => <li key={user.id}>{user.name}</li>)}
</ul>
);
}The if (!users) guard prevents rendering the list before data is available. This also provides a better user experience with a loading indicator.
With explicit loading state:
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch("/api/users")
.then(res => res.json())
.then(data => {
setUsers(data);
setLoading(false);
});
}, []);
if (loading) return <p>Loading...</p>;Fix 3: Use Optional Chaining
Use ?. to safely access the array before calling .map():
{users?.map(user => <li key={user.id}>{user.name}</li>)}If users is undefined or null, the entire expression evaluates to undefined, which React ignores (renders nothing). No error.
With a fallback:
{(users ?? []).map(user => <li key={user.id}>{user.name}</li>)}The ?? (nullish coalescing) operator returns [] if users is null or undefined.
Fix 4: Fix API Response Structure
The API might return data in a nested structure:
{
"data": {
"users": [
{"id": 1, "name": "Alice"},
{"id": 2, "name": "Bob"}
]
},
"meta": {
"total": 2
}
}Broken, accessing the wrong level:
const [users, setUsers] = useState([]);
useEffect(() => {
fetch("/api/users")
.then(res => res.json())
.then(data => setUsers(data)); // data is the whole response object!
}, []);
// users.map() fails because users is {data: {users: [...]}, meta: {...}}Fixed, extract the array:
useEffect(() => {
fetch("/api/users")
.then(res => res.json())
.then(response => setUsers(response.data.users)); // Extract the array
}, []);Debug the response shape:
useEffect(() => {
fetch("/api/users")
.then(res => res.json())
.then(data => {
console.log("API response:", data);
console.log("Is array?", Array.isArray(data));
setUsers(data);
});
}, []);For rendering objects directly in JSX, see Fix: Objects are not valid as a React child.
The assumption that bites people here is that the API returns an array at the top level. Most REST APIs do not, they wrap the array in an envelope like { data: [...] }, { results: [...] }, or { items: [...] }. Log the real response once and confirm the exact path to the array before you set state; guessing the shape is how the defined-but-not-an-array version of this crash happens.
Fix 5: Set Default Prop Values
If the array comes from props, provide a default:
Broken:
function ItemList({ items }) {
return (
<ul>
{items.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
);
}
// Parent forgets to pass items:
<ItemList />Fixed, default parameter:
function ItemList({ items = [] }) {
return (
<ul>
{items.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
);
}Fixed, with TypeScript:
interface Props {
items?: Item[];
}
function ItemList({ items = [] }: Props) {
return (
<ul>
{items.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
);
}The items = [] default handles both undefined and missing props.
Fix 6: Fix Context and Redux State
If the array comes from context or Redux:
Broken, context value not initialized:
const DataContext = createContext(); // undefined by default!
function ItemList() {
const { items } = useContext(DataContext);
// With no Provider above, useContext returns undefined and the destructure
// itself throws: "Cannot destructure property 'items' of ... as it is undefined"
return items.map(...);
// With a Provider that passes an object *missing* items, you get past the
// destructure and crash here instead: "Cannot read properties of undefined (reading 'map')"
}Which line throws tells you which of the two mistakes you made.
Fixed, provide a default value:
const DataContext = createContext({ items: [] });Redux, handle initial state:
const initialState = {
users: [], // Always initialize arrays
loading: false,
error: null,
};
function usersReducer(state = initialState, action) {
switch (action.type) {
case "FETCH_SUCCESS":
return { ...state, users: action.payload, loading: false };
default:
return state;
}
}Fix 7: Handle Error States
API calls can fail, leaving state as the initial value:
const [users, setUsers] = useState([]);
const [error, setError] = useState(null);
useEffect(() => {
fetch("/api/users")
.then(res => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
})
.then(data => setUsers(data))
.catch(err => setError(err.message));
}, []);
if (error) return <p>Error: {error}</p>;
return (
<ul>
{users.map(user => <li key={user.id}>{user.name}</li>)}
</ul>
);Without error handling, a failed API call leaves users as [] (if initialized correctly) or could set it to an unexpected value.
For useEffect dependency issues that cause infinite loops during data fetching, see Fix: React useEffect infinite loop.
Fix 8: Use Array.isArray() for Type Safety
When you are not sure if a value is an array:
function SafeList({ data }) {
const items = Array.isArray(data) ? data : [];
return (
<ul>
{items.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
);
}Array.isArray() returns true only for actual arrays, not for objects, strings, or other types that might have a .map property.
Subtler Sources of the Crash
Check for race conditions. Two adjacent setUsers calls in the same tick are batched by React 18 into a single render, so a back-to-back setUsers(undefined); setUsers(newData) will not crash. The dangerous version is when the reset and the data arrival happen in different ticks:
// Route change resets the list...
setUsers(undefined); // render #1: users is undefined — .map crashes here
// ...while an in-flight fetch from the previous route resolves later:
fetchUsers().then(setUsers); // render #2 never happens if render #1 threwReset to [] instead of undefined, and cancel or ignore stale fetches (an AbortController or an ignore flag in the effect cleanup) so late responses cannot interleave with resets.
Check for data transformation bugs. A .filter() or .find() in the chain might return undefined:
// .find() returns undefined if no match
const activeUsers = users.find(u => u.active);
activeUsers.map(...) // TypeError if no active users found!
// Fix: Use .filter() which always returns an array
const activeUsers = users.filter(u => u.active);
activeUsers.map(...) // Works — empty array if no matchesCheck for stale closures. An async operation might capture an old reference to state. A captured state from a previous render can resolve after a fresh render replaced the array with undefined, and the late callback then writes the wrong value back.
Check for SSR rendering before data loads. In Next.js App Router, a Client Component starts with the initial state on the server and re-renders on the client. If your fetch only runs in useEffect, the SSR pass sees undefined. Move the fetch into a Server Component or a loader (Remix) so the data is present on the first render. See Fix: Next.js params should be awaited for async data patterns specific to the App Router.
Check for double-render under React Strict Mode. In development, Strict Mode invokes function components twice and runs effects twice. If your effect mutates external state in a way that briefly sets it to undefined, the second render can crash where the first one would not. The fix is to make state transitions idempotent, never go from “real array” back through undefined.
Check for a missing return in a function that builds the array. A block-body arrow that forgets to return yields undefined, and .map on that throws. () => { rows } returns undefined; you want () => rows or () => { return rows; }. This bites most often when you refactor a one-line arrow into a multi-line block and drop the implicit return, the helper still “looks” like it produces an array but silently hands back undefined.
Check for JSON.parse returning non-arrays. Reading from localStorage and parsing the result returns whatever was stored. If the stored value is the string "null", you get null. If it is empty, JSON.parse("") throws. Always guard with a try/catch and a default.
For the general version of this error (not specific to .map), see Fix: TypeError: Cannot read properties of undefined. For the closely related warning when keys are missing on rendered list items, the cause and fix sit in the same render path described above.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: AutoAnimate Not Working — Transitions Not Playing, List Items Not Animating, or React State Changes Ignored
How to fix @formkit/auto-animate issues — parent ref setup, React useAutoAnimate hook, Vue directive, animation customization, disabling for specific elements, and framework integration.
Fix: Blurhash Not Working — Placeholder Not Rendering, Encoding Failing, or Colors Wrong
How to fix Blurhash image placeholder issues — encoding with Sharp, decoding in React, canvas rendering, Next.js image placeholders, CSS blur fallback, and performance optimization.
Fix: Docusaurus Not Working — Build Failing, Sidebar Not Showing, or Plugin Errors
How to fix Docusaurus issues — docs and blog configuration, sidebar generation, custom theme components, plugin setup, MDX compatibility, search integration, and deployment.
Fix: Embla Carousel Not Working — Slides Not Scrolling, Autoplay Not Starting, or Thumbnails Not Syncing
How to fix Embla Carousel issues — React setup, slide sizing, autoplay and navigation plugins, loop mode, thumbnail carousels, responsive breakpoints, and vertical scrolling.