React: Iteration

It's the fifth anniversary of my little programming blog! 🥹 If you haven't started (and stuck to) a blog yet, I highly recommend it. The older posts on this site are so hard to read, but it is special to have a little corner of the internet that I've built and that reflects my growth and learning.

To mark the occasion, here's a picture of me performing with Little Mazarn at Austin Psych Fest earlier this month.

Joey, Lindsey, and Jeff performing as Little Mazarn at Austin Psych Fest in May of 2023

Enough with the sentimentality. I'm once again writing about React today.

In my last post, I had an example where I rendered each item from an array as a document node. This is a practice called iteration.

function App() {
  const tableOfContents = [
    "Introduction",
    "Ingredients",
    "Directions",
    "Recipe modifications",
  ];
  return (
    <ol>
      {tableOfContents.map(item => <li>{item}</li>)}
    </ol>
  );
}

export default App;

You may be wondering, why not just use a good old fashioned for Loop? That might look something like this:

function App() {
  const tableOfContents = [
    "Introduction",
    "Ingredients",
    "Directions",
    "Recipe modifications",
  ];
  return (
    <ol>
      {for (let i = 0; i < tableOfContents.length; i += 1) {
        return (
          <li>{tableOfContents[i]}</li>
        );
      }}
    </ol>
  );
}

export default App;

The issue once again is that React Components written in JSX transpile down to a series of plain JavaScript expressions. The JSX in the previous example would transpile into something like:

React.createElement(
  'ol',
  {},
  for (let i = 0; i < tableOfContents.length; i += 1) {
    return (
      React.createElement(
        'li',
        {},
        tableOfContents[i]
      )
    );
  }
);

In this case we're using a statement, the for Loop, where an expression is expected.

Instead, the common practice is to use the .map() array method:

function App() {
  const tableOfContents = [
    "Introduction",
    "Ingredients",
    "Directions",
    "Recipe modifications",
  ];
  return (
    <ol>
      {tableOfContents.map(item => <li>{item}</li>)}
    </ol>
  );
}

export default App;

This code transpiles to something like:

React.createElement(
  "ol",
  {},
  tableOfContents.map(item => (
    React.createElement(
      "li",
      {},
      item
    )
  ))
);

It's also common to use .reduce(), .filter() and other array methods in conjunction with .map().

Keys

Looping (or, more accurately, mapping) over an array of items and displaying them as DOM nodes is the visible concern when it comes to iteration in React. The other concern is less apparent, but has to do with how React keeps track of its state over the lifespan of the application.

In all of the iteration examples so far, if you were to run that code as-is, you would get the following warning in the console or the terminal:

Warning: Each child in a list should have a unique "key" prop.

What React is complaining about is that you need to attach a key prop to each list item so that React can better optimize DOM changes when that list updates. Data can change in a variety of different ways in a React app. User interactions may drive state changes or the data may change in the API. An array of data might be reordered, or have items added or removed from it. Those items might be added or removed from the end of the array, from the beginning, or somewhere in the middle.

The point here is that there are a large number of ways that data can change in React, and those changes will often need to reflect in the UI. React does a lot of work to optimize those change operations, and those optimization efforts rely on array items having unique key props when they are rendered as DOM nodes. It's worth nothing that React can handle UI update operations without the unique keys, but those key props help it determine how to optimize certain operations.

Usually when you are getting an array of content out of a database or API, it will come with a unique ID. This is the ideal solution. Your data may look something like the following:

const tableOfContents = [
  {
    content: "Introduction",
    id: "abcd-efgh-ijkl-0"
  },
  {
    content: "Ingredients",
    id: "abcd-efgh-ijkl-1"
  },
  {
    content: "Directions",
    id: "abcd-efgh-ijkl-2"
  },
  {
    content: "Recipe modifications",
    id: "abcd-efgh-ijkl-3"
  },
];

In this case, you can just use the id on each array item:

function App() {
  const tableOfContents = [
    {
      content: "Introduction",
      id: "abcd-efgh-ijkl-0"
    },
    {
      content: "Ingredients",
      id: "abcd-efgh-ijkl-1"
    },
    {
      content: "Directions",
      id: "abcd-efgh-ijkl-2"
    },
    {
      content: "Recipe modifications",
      id: "abcd-efgh-ijkl-3"
    },
  ];
  return (
    <ol>
      {tableOfContents.map(item => <li key={item.id}>{item.content}</li>)}
    </ol>
  );
}

export default App;

Unfortunately, not all arrays come with such unique identifiers. A common solution is to lean into the optional index argument that comes with .map():

function App() {
  const tableOfContents = [
    "Introduction",
    "Ingredients",
    "Directions",
    "Recipe modifications",
  ];
  return (
    <ol>
      {tableOfContents.map((item, i) => <li key={i}>{item}</li>)}
    </ol>
  );
}

export default App;

In most cases this is a perfectly fine solution. But if the list items are getting rearranged through state changes, it may cause issues. In this case, "Ingredients" will have the key prop value of 1 and "Recipe modifications" will have the key prop value of 3. However, say "Recipe modifications" switches places with "Ingredients" in the array. The key prop values will no longer be consistent with the array items. This can cause weird re-render issues or other bugs that are hard to diagnose.

There's no great solution here. One approach that Josh Comeau highlights in his course is to use crypto.randomUUID(), which generates a random ID value similar to what you might get from an array. This approach might look something like the following:

function App() {
  const tableOfContents = [
    {
      content: "Introduction",
      id: crypto.randomUUID(),
    },
    {
      content: "Ingredients",
      id: crypto.randomUUID(),
    },
    {
      content: "Directions",
      id: crypto.randomUUID(),
    },
    {
      content: "Recipe modifications",
      id: crypto.randomUUID(),
    },
  ];
  return (
    <ol>
      {tableOfContents.map(item => <li key={item.id}>{item.content}</li>)}
    </ol>
  );
}

export default App;

One last note about the key prop is that this prop value only needs to be unique within its array, not globally. It's perfectly fine to have two array items with the same key prop value, as long as they are in different arrays.

Review

  • Use .map() to iterate over array items in React, not for Loops. You can combine .map() with other array methods, like .filter() and .reduce()
  • React strongly prefers you give each item in the array that gets rendered as a DOM node a unique key prop.
    • You can use the array index if the items aren't going to be reordered. If they are reordered, then he index may change but the key in the app component will not, causing very subtle, hard to diagnose bugs
    • The key prop needs to be applied to the top-level element within the .map() call
      • If your top-level element is a shorthand fragment <></>, you may need to change it to <React.Fragment key={key}></React.Fragment>
    • The key prop does not need to be globally unique, just unique within its array