TypeOfNaN

Fix the "Cannot read property 'map' of undefined" Error in React

Nick Scialli February 21, 2021🚀 4 minute read

If you're enjoying this blog, please consider:

This is one of the more common errors you will run into when starting out with React:

Cannot read property 'map' of undefined

In this post, we’ll learn how to fix it.

Why It’s Happening

The variable you are trying to map over is undefined. It will probably eventually be an array, but due to the asynchronous nature of React, you are experiencing at least one render when the variable is undefined.

Let’s take this example code. In it, we fetch some data from an API and set state with that data.

function MyComponent() {
  const [data, setData] = useState();

  useEffect(() => {
    fetch('/api/data')
      .then((res) => res.json())
      .then((data) => {
        setData(data);
      })
      .catch((err) => {
        console.log(err);
      });
  }, []);

  return (
    <ul>
      {data.map((item) => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
}

This code might seem fine, but our data fetching takes some time and React does not wait for the data fetching (or any async action) to happen before it first renders your JSX. That means, while the data is being fetched, React is trying to run data.map(...).

Since we provided no initial value for data in our useState hook, data is undefined. As we know from the error message, it’s problematic to try to call map on undefined!

Fix Option 1: Default the Variable to an Empty Array

This first quick fix might be enough for your use case: just default your stateful variable to an array while you’re waiting for your data to fetch. For example:

function MyComponent() {
  // Empty array in useState!
  const [data, setData] = useState([]);

  useEffect(() => {
    fetch('/api/data')
      .then((res) => res.json())
      .then((data) => {
        setData(data);
      })
      .catch((err) => {
        console.log(err);
      });
  }, []);

  return (
    <ul>
      {data.map((item) => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
}

The reason this works is that, while your data fetching is happening, React will call the map method on an empty data array. This is fine—nothing will be rendered and there will be no errors. Once the data loads from the API call, our data state will be set and our list will correctly render.

Fix Option 2: Showing a Loading Indicator

While the previous fix is simple, it might not be the best user experience to display nothing until the data loads. Instead, we might choose to display a loading indicator. There are a few ways we can do this, but one of the simpler ways is to just add another stateful variable called loading.

function MyComponent() {
  const [data, setData] = useState([]);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    setLoading(true);
    fetch('/api/data')
      .then((res) => res.json())
      .then((data) => {
        setData(data);
      })
      .catch((err) => {
        console.log(err);
      })
      .finally(() => {
        setLoading(false);
      });
  }, []);

  if (loading) {
    return <p>Data is loading...</p>;
  }

  return (
    <ul>
      {data.map((item) => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
}

This is pretty simple and effective! When our data starts to fetch, we set loading to true. When it’s done fetching, we set loading to false. Note that we use the finally method on our Promise since that will run regardless of whether the fetch succeeds or fails.

Speaking of Fetch Failures…

We should probably handle the situation in which our fetch fails. Additionally, we can show the user an error message if our data variable is not an array. This latter point is important in making sure that we never try to access the map property on a non-array since it simply won’t work.

function MyComponent() {
  const [data, setData] = useState([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState();

  useEffect(() => {
    setLoading(true);
    fetch('/api/data')
      .then((res) => res.json())
      .then((data) => {
        setData(data);
      })
      .catch((err) => {
        setError(err);
      })
      .finally(() => {
        setLoading(false);
      });
  }, []);

  if (loading) {
    return <p>Data is loading...</p>;
  }

  if (error || !Array.isArray(data)) {
    return <p>There was an error loading your data!</p>;
  }

  return (
    <ul>
      {data.map((item) => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
}

And now we have a pretty safe way of handling our async operation without getting the dreaded “cannot read property ‘map’ of undefined” error!


Nick Scialli

Nick Scialli is a software engineer at the U.S. Digital Service.

Subscribe to the mailing list!

If you like what I post here, please sign up to get updates and code insights in your inbox. I won't spam you and you can unsubscribe any time!

Powered by Buttondown.