loading...
Cover image for React - Cannot read property 'map' of undefined

React - Cannot read property 'map' of undefined

sag1v profile image Sagiv ben giat Updated on ・4 min read

Originally posted on my personal blog debugger.io

If you are a react developer, there is a good chance that you faced this error couple of times:

TypeError: Cannot read property 'map' of undefined

TL;DR - If you are not in the mode for reading or you just want the bottom line, then here it is

The problem

In order to understand what are the possible solutions, lets first understand what is the exact issue here.

Consider this code block:

// Just a data fetching function
const fetchURL = "https://jsonplaceholder.typicode.com/todos/";
const getItems = () => fetch(fetchURL).then(res => res.json());

function App() {
  const [items, setItems] = useState();

  useEffect(() => {
    getItems().then(data => setItems(data));
  }, []);

  return (
    <div>
      {items.map(item => (
        <div key={item.id}>{item.title}</div>
      ))}
    </div>
  );
}

We have a component that manage a state of items, it also have an effect which inside it we run an asynchronous operation - getItems, which will return us the data we need from the server, then we call setItems with the received data as items.
This component also renders the items - it iterate over it with .map and returning a react element for each item.

But we wont see anything on the screen, well except the error:

TypeError: Cannot read property 'map' of undefined

What's going on here?

We do have an items variable:

const [items, setItems] = useState();

And we did populate it with our data returned from the server:

useEffect(() => {
    getItems().then(data => setItems(data));
  }, []);

Well lets examine how the react flow looks like in our example:

  1. React renders (invoking) our component.
  2. React "see" the useState call and return us [undefined, fn].
  3. React evaluate our return statement, when it hits the items.map(...) line its actually running undefined.map(...) which is obviously an error in JavaScript.

What about our useEffect call though?

React will run all effects after the render is committed to the screen, which means we can't avoid a first render without our data.

Possible solutions

#1 Initial value

One possible solution is to give your variable a default initial value, with useState it would look like that:

const [items, setItems] = useState([]);

This means that when react runs our useState([]) call, it will return us with

[[], fn]

Which means that in the first render of our component, react will "see" our items as an empty array, so instead of running undefined.map(...) like before, it will run [].map(...).

#2 Conditional rendering

Another possible solution is to conditionally render the items, meaning if we have the items then render them, else don't render (or render something else).

When working with JSX we can't just throw some if else statements inside our tree:

// ⚠️ wont work!!
export default function App() {
  // ....
  return (
    <div>
      {
        if(items){
          items.map(item => (
            <div key={item.id}>{item.title}</div>
          ))
        }
      }
    </div>
  );
}

But instead we can create a variable outside our tree and populate it conditionally:

Note that we removed the initial array for items.

function App() {
  const [items, setItems] = useState();

  useEffect(() => {
    getItems().then(data => setItems(data));
  }, []);

  let itemsToRender;
  if (items) {
    itemsToRender = items.map(item => {
      return <div key={item.id}>{item.title}</div>;
    });
  }

  return <div>{itemsToRender}</div>;
}

The undefined or null values are ignored inside the context of JSX so its safe to pass it on for the first render.

We could also use an else statement if we want to render something else like a spinner or some text:

function App() {
  const [items, setItems] = useState();

  useEffect(() => {
    getItems().then(data => setItems(data));
  }, []);

  let itemsToRender;
  if (items) {
    itemsToRender = items.map(item => {
      return <div key={item.id}>{item.title}</div>;
    });
  } else {
    itemsToRender = "Loading...";
  }

  return <div>{itemsToRender}</div>;
}

#2.5 Inline conditional rendering

Another option to conditionally render something in react, is to use the && logical operator:

function App() {
  const [items, setItems] = useState();

  useEffect(() => {
    getItems().then(data => setItems(data));
  }, []);

  return (
    <div>
      {items && items.map(item => {
          return <div key={item.id}>{item.title}</div>;
        })}
    </div>
  );
}

Why it works? The react docs explains it well:

It works because in JavaScript, true && expression always evaluates to expression, and false && expression always evaluates to false.
Therefore, if the condition is true, the element right after && will appear in the output. If it is false, React will ignore and skip it.

We can also use the conditional operator condition ? true : false if we want to render the Loading... text:

function App() {
  const [items, setItems] = useState();

  useEffect(() => {
    getItems().then(data => setItems(data));
  }, []);

  return (
    <div>
      {items
        ? items.map(item => {
            return <div key={item.id}>{item.title}</div>;
          })
        : "Loading..."}
    </div>
  );
}

We can also mix both solutions, i.e: initial value with conditional rendering:

function App() {
  const [items, setItems] = useState([]);

  useEffect(() => {
    getItems().then(data => setItems(data));
  }, []);

  return (
    <div>
      {items && items.length > 0
        ? items.map(item => {
            return <div key={item.id}>{item.title}</div>;
          })
        : "Loading..."}
    </div>
  );
}

Though keep in mind, whenever conditions become too complex, it might be a signal for us to extract that logic to a component:

function List({ items, fallback }) {
  if (!items || items.length === 0) {
    return fallback;
  } else {
    return items.map(item => {
      return <div key={item.id}>{item.title}</div>;
    });
  }
}

function App() {
  const [items, setItems] = useState([]);

  useEffect(() => {
    getItems().then(data => setItems(data));
  }, []);

  return (
    <div>
      <List items={items} fallback={"Loading..."} />
    </div>
  );
}

Wrapping up

When we get such an error, we are probably getting the value in an asynchronous way. We should provide an initial value for our variable or conditionally render it or both. If our condition become too complex, it might be a good time to extract the logic to a component.

Hope you found this article helpful, if you have a different approach or any suggestions i would love to hear about them, you can tweet or DM me @sag1v. 🤓

For more articles you can visit debuggr.io

Posted on by:

sag1v profile

Sagiv ben giat

@sag1v

An enthusiastic developer with a great passion to the front-end echo system. Personal blog debuggr.io

Discussion

pic
Editor guide
 

You can use an immediately-invoked function expression to add an if statement to JSX, but I don’t recommend it if a ternary or the && operator can be used. Link: react-cn.github.io/react/tips/if-e...