Get eBook BundleVolume I index
JSX Is Real React Code

Arrays in JSX

Ishtmeet Singh @ishtms/July 20, 2026/11 min read
#react#jsx#arrays#lists#map

Most lists in React start with some data sitting in an array.

Maybe you've got lessons coming from an API, maybe hardcoded for now, doesn't really matter. Say we've got this:

jsx
const lessons = [
  { id: "expressions", title: "Expressions Inside JSX" },
  { id: "attributes", title: "Attributes and Styles" },
  { id: "fragments", title: "Fragments and Nested Trees" },
];

Now these are just plain JavaScript objects. React can't take one of these objects and directly show it on the page as a child.

jsx
<ul>{lessons}</ul>

Yeah, that's not gonna work.

We somehow need to take every lesson object and turn it into something React can render, maybe an li. And if you've done JavaScript before, you already know the method we need here: map.

Read map as normal JavaScript first

Before putting map inside JSX, let's just use it normally.

jsx
const items = lessons.map((lesson) => (
  <li key={lesson.id}>{lesson.title}</li>
));

What happened here?

map calls our callback once for every lesson in the array. First lesson goes in, one li comes back. Second lesson goes in, another li comes back. Same for the third one.

Then map collects all those returned values into a new array.

So after this code runs, lessons still contains the original lesson objects and items contains React elements.

jsx
return <ul>{items}</ul>;

React can render that array of elements as children of the ul.

And this is probably the main thing to get before we continue: map is still just JavaScript here. React isn't adding some special list version of map. You're using normal Array.prototype.map(), and the callback just happens to return React elements.

You can put the map directly inside JSX

We don't really need the items variable if we're only using it once.

So instead of:

jsx
const items = lessons.map((lesson) => (
  <li key={lesson.id}>{lesson.title}</li>
));

return <ul>{items}</ul>;

you'll very often see:

jsx
return (
  <ul>
    {lessons.map((lesson) => (
      <li key={lesson.id}>{lesson.title}</li>
    ))}
  </ul>
);

Same operation.

Those outer {} mean we're entering JavaScript expression mode inside JSX, and the expression happens to be a map() call. That call returns an array of React elements, which React then renders inside the ul.

Personally, if the mapping is this small, inline is pretty easy to read. But once you start filtering, sorting, debugging, checking empty results and doing five other things before rendering, pulling some of that work above the JSX usually makes the component much easier to follow.

Your callback still has to return something

This bug gets almost everybody at some point.

This works:

jsx
lessons.map((lesson) => (
  <li key={lesson.id}>{lesson.title}</li>
));

Because with the parentheses form, that JSX expression is returned from the callback.

This also works:

jsx
lessons.map((lesson) => {
  return <li key={lesson.id}>{lesson.title}</li>;
});

We used {} for the function body, so now we wrote return ourselves.

But this one?

jsx
lessons.map((lesson) => {
  <li key={lesson.id}>{lesson.title}</li>;
});

Nothing gets rendered.

And this can be annoying because the JSX looks completely fine. No syntax error, no screaming red terminal output, nothing obvious.

The problem is that {} starts a normal function body. Since there's no return, every callback call returns undefined. So map gives us an array containing undefined values, and React has no list items to render from that.

If you use parentheses, the expression is returned automatically. If you use braces, remember the return.

The HTML still needs to make sense

React doesn't remove normal HTML rules just because you're generating elements with map.

If you've got a ul, its items should be li elements.

jsx
<ul>
  {lessons.map((lesson) => (
    <li key={lesson.id}>{lesson.title}</li>
  ))}
</ul>

Pretty normal.

But don't do this:

jsx
<ul>
  {lessons.map((lesson) => (
    <article key={lesson.id}>{lesson.title}</article>
  ))}
</ul>

The fact that those article elements came from an array changes nothing about the HTML structure.

If every lesson needs an article, put it inside the li:

jsx
<li key={lesson.id}>
  <article>{lesson.title}</article>
</li>

Or if what you're rendering isn't really a list semantically, then maybe you don't want the ul there in the first place.

React generates elements. Browser HTML rules still apply after that.

Mapping to your own component

At some point your list item usually grows beyond one line.

Maybe now every lesson needs a heading and its duration:

jsx
function LessonItem({ lesson }) {
  return (
    <li>
      <h2>{lesson.title}</h2>
      <p>{lesson.minutes} min</p>
    </li>
  );
}

Then the list can map each lesson to that component instead:

jsx
function LessonList({ lessons }) {
  return (
    <ul>
      {lessons.map((lesson) => (
        <LessonItem key={lesson.id} lesson={lesson} />
      ))}
    </ul>
  );
}

Notice the key is on LessonItem.

That's because those LessonItem elements are what map is returning as siblings. React uses the keys while matching those sibling elements between renders.

Inside LessonItem, we return the actual li.

You don't need to extract every tiny mapped item into a component just because you can, by the way. If your entire item is this:

jsx
<li>{lesson.title}</li>

I'd probably leave it there.

Once the item starts having its own markup, state, events, or logic, then giving it a component usually starts making more sense.

Filtering before mapping

Now suppose we don't want every lesson.

Maybe only published lessons should appear.

filter() is useful here because it gives us another array containing only the records that passed our condition.

jsx
const publishedLessons = lessons.filter(
  (lesson) => lesson.published,
);

At this point publishedLessons still contains lesson objects.

Then we map those objects into elements:

jsx
const items = publishedLessons.map((lesson) => (
  <LessonItem key={lesson.id} lesson={lesson} />
));

So filter answers, "which lesson records do I want?"

Then map answers, "what React element should each of those records become?"

You can chain them too:

jsx
const items = lessons
  .filter((lesson) => lesson.published)
  .map((lesson) => (
    <LessonItem key={lesson.id} lesson={lesson} />
  ));

Nothing special going on there. filter() returns an array, then map() runs on that returned array.

I tend to keep a separate variable when I need that filtered data for something else as well, though.

For example, the count.

Count the same data you're actually showing

Say we've got six lessons total, but after applying the current filter only two are visible.

This would be wrong:

jsx
<p>{lessons.length} lessons</p>

Because the page says six lessons while the user can only see two.

Better:

jsx
const visibleLessons = lessons.filter(matchesCurrentFilter);
const visibleCount = visibleLessons.length;

Then use the same visibleLessons array for both things:

jsx
return (
  <section>
    <p>{visibleCount} lessons</p>
    <LessonList lessons={visibleLessons} />
  </section>
);

Now the text and the rendered list are both based on the exact same filtered data.

This sounds obvious when looking at a five-line example. In a bigger component, it's pretty easy to filter something in one place and accidentally calculate a count from the original array somewhere else.

What if the array is empty?

map has no problem with an empty array.

jsx
[].map((lesson) => <LessonItem lesson={lesson} />)

You just get another empty array back.

React can render that too, which means you'll see... nothing.

Sometimes nothing is exactly what you want. Most of the time for an actual user-facing list, it's probably confusing.

If somebody applied a filter and no lessons matched, tell them that:

jsx
if (visibleLessons.length === 0) {
  return <p>No lessons match this filter.</p>;
}

Then if we do have lessons:

jsx
return <LessonList lessons={visibleLessons} />;

Where should that empty message live?

Usually in the component that actually knows why there's no data.

A reusable LessonList may have no idea whether the list is empty because nothing has been published yet, because a search found no matches, because some category filter removed everything, etc.

If several places use the same list component and you want the list itself to handle this, you can also pass something such as an emptyMessage prop.

The empty array itself isn't an error. You just need to decide what the page should show when that's the current data.

Don't sort props directly

Now we want alphabetical lessons.

You might be tempted to do this:

jsx
function LessonList({ lessons }) {
  lessons.sort((a, b) => a.title.localeCompare(b.title));

  // ...
}

Don't.

sort() changes the array you call it on.

And lessons came in through props, which means some parent gave us that array. Another component may also be using the same array reference. If we reorder it here during render, we've changed data that this component doesn't own.

Modern JavaScript gives us toSorted():

jsx
const sortedLessons = lessons.toSorted(
  (a, b) => a.title.localeCompare(b.title),
);

That returns another array and leaves lessons alone.

If you're targeting an environment without toSorted(), copy the array first:

jsx
const sortedLessons = [...lessons].sort(
  (a, b) => a.title.localeCompare(b.title),
);

One thing though: [...lessons] only copies the array itself.

The lesson objects inside are still the same objects.

So sorting sortedLessons is fine because we're only changing the order of the copied array. But doing this:

jsx
sortedLessons[0].title = "Changed";

would still modify that original lesson object too.

So during render, stick with operations that produce new results instead of changing incoming data.

Methods such as filter(), map(), toSorted() and toReversed() return new arrays. Methods such as sort(), reverse(), splice(), push() and pop() modify the array they're called on.

Don't create a new ID inside map

This one looks reasonable at first:

jsx
lessons.map((lesson) => (
  <LessonItem
    key={crypto.randomUUID()}
    lesson={lesson}
  />
));

We've got keys. They're unique. Great?

Nope.

Every time this component renders, crypto.randomUUID() creates completely new IDs.

So the exact same lesson gets a different key on every render.

React uses that key to identify an item across renders. If you keep changing it, React can't match the previous LessonItem with the next LessonItem for that lesson.

If a lesson needs an ID, create it when the lesson record itself is created:

jsx
const newLesson = {
  id: crypto.randomUUID(),
  title: "New lesson",
};

Then later:

jsx
<LessonItem key={lesson.id} lesson={lesson} />

Now that lesson keeps the same ID between renders.

Your map should usually be taking the data you already have and returning UI for it. Generating a new identity for the same record every time the component runs just causes problems.

Arrays can contain more than one kind of renderable value

React children don't all have to be elements.

For example:

jsx
const content = [
  "Chapter 3",
  <strong key="title">JSX</strong>,
];

React can render that array.

Arrays can also contain nested arrays of renderable children, and React processes those into the resulting child sequence.

You can use all of that, but normal application lists are usually much easier to understand when every item follows the same setup: one data record comes in, one predictable bit of UI comes out.

If your data itself is nested, it's usually better to be clear about that in the data and rendering code instead of depending on confusing nested arrays of children.

Future-you will appreciate it.

Probably.

Putting the whole thing together

Suppose we want only published lessons, sorted by their order.

We can calculate that before the JSX:

jsx
const visibleLessons = lessons
  .filter((lesson) => lesson.published)
  .toSorted((a, b) => a.order - b.order);

If there aren't any:

jsx
if (visibleLessons.length === 0) {
  return <p>No published lessons.</p>;
}

And if there are:

jsx
return (
  <ul>
    {visibleLessons.map((lesson) => (
      <LessonItem key={lesson.id} lesson={lesson} />
    ))}
  </ul>
);

The original lessons array hasn't been reordered or edited here.

filter() gave us another array containing only the published lessons. toSorted() gave us another array in the order we wanted. Then map() turned each lesson record into a LessonItem.

That's really most React list rendering.

You start with data, decide which records you actually want, maybe order them, then map those records into React elements.

If nothing is left, show something useful instead of just leaving a blank area.

And when the list looks weird or nothing appears, don't immediately blame React. Check what array you actually produced, check whether your map callback returned anything, check the HTML you're returning, and check whether your keys stay the same between renders.

Half the time, the problem is sitting right there in normal JavaScript.

Sources