Turning an Array into Elements
Almost every real screen renders a list: search results, cart items, comments, a table of marks. React has no loop syntax of its own for this, and does not need one. Because JSX renders an array by rendering each element in it, the ordinary map method is all you need — it takes an array of data and returns an array of JSX elements.
Read the pattern once and it becomes automatic: {items.map(item => <li>{item.name}</li>)}. The braces drop you into JavaScript, map produces an array, and React renders every element of that array in order. There is no template directive to memorise and no special syntax — it is the same map you already use.
One detail catches people who write the arrow function with braces. item => (<li>...</li>) returns the element, because parentheses after an arrow mean return this expression. But item => { <li>...</li> } is a function body with a statement in it and no return, so it returns undefined for every item and your list renders empty. If a list is mysteriously blank, check that first.
function MarksTable({ students }) {
return (
<table>
<tbody>
{students.map(student => (
<tr key={student.rollNo}>
<td>{student.rollNo}</td>
<td>{student.name}</td>
<td>{student.marks}</td>
</tr>
))}
</tbody>
</table>
);
}
// Returns elements — correct
students.map(s => <li key={s.id}>{s.name}</li>)
students.map(s => (<li key={s.id}>{s.name}</li>))
students.map(s => { return <li key={s.id}>{s.name}</li>; })
// Returns undefined — renders nothing, warns about nothing
students.map(s => { <li key={s.id}>{s.name}</li>; }) - You will also see
mapused to render a list of components rather than plain tags:{products.map(p => <ProductCard key={p.id} product={p} />)}. It is the same mechanism — an array of elements is an array of elements, whether they are<li>tags or your own components.
What the key Prop Is Actually For
Every element produced by map needs a key prop, and React warns loudly in the console when one is missing. The warning is easy to silence and easy to silence wrongly, so it is worth understanding what React does with the key.
When a list re-renders, React has an old array of elements and a new one, and it has to decide what to do with the real DOM nodes it already created. Should it update the existing third row, or throw it away and build a new one? Without a key it can only go by position, which means it assumes the item at index 2 before and the item at index 2 after are the same thing. With a key, React can recognise items across renders: same key means same item, so update it in place; new key means a new item, so create it; a key that has vanished means the item is gone, so remove it.
This is not only about speed. React attaches things to DOM nodes that you cannot see in your data — the text typed into an uncontrolled input, which element has focus, scroll position inside a container, a CSS transition in progress, and the internal state of any component in the list. Keys are how React knows which of those belongs to which item. Get them wrong and that hidden state attaches to the wrong row.
// Before: keys tell React which DOM node belongs to which item
// [ {id:'b', ...}, {id:'c', ...} ]
// After inserting at the front:
// [ {id:'a', ...}, {id:'b', ...}, {id:'c', ...} ]
// With key={item.id}, React sees: 'a' is new -> insert one node.
// The nodes for 'b' and 'c' are reused, untouched.
// With key={index}, React sees: index 0 changed from b to a,
// index 1 changed from c to b, index 2 is new.
// -> it rewrites every row. - Keys only need to be unique among siblings, not across your whole app. Two different lists on the same page may both use keys 1, 2 and 3 without any conflict.
Why Index-as-Key Breaks Things
key={index} makes the warning go away, which is why so much code contains it. It is also silently wrong the moment your list can be reordered, or have items inserted or removed anywhere except the end.
Here is the concrete failure, and it is worth building once so you never forget it. Take a to-do list where each row has a checkbox and the text. Tick the checkbox on the row that says Pay electricity bill. Now add a new task, and suppose new tasks go to the top. The new task is now at index 0, and Pay electricity bill has moved to index 1. React, keying by index, believes the item at index 0 is the same item it rendered before — so it keeps that row's DOM node, including the ticked checkbox, and merely updates the text. The tick is now sitting on the new task, and the bill is unticked. Your data is completely correct. Only the screen is wrong.
The same thing happens with uncontrolled inputs in a list, with a component that holds its own useState, with focus, and with animations. Delete the middle item of a five-row form and the text from row four appears in row three. These are maddening bugs to chase because nothing in your state is wrong.
Index keys are acceptable in exactly one situation: a list that never reorders, never has insertions or deletions in the middle, and whose items hold no state of their own. A static footer menu qualifies. A to-do list, a cart or a sortable table does not. When in doubt, do not use the index.
// BROKEN when tasks can be added at the top or removed from the middle
{tasks.map((task, index) => (
<li key={index}>
<input type="checkbox" /> {/* uncontrolled: React owns this tick */}
{task.text}
</li>
))}
// CORRECT: a key that belongs to the item, not to its position
{tasks.map(task => (
<li key={task.id}>
<input type="checkbox" />
{task.text}
</li>
))}
// Give every item an id when it is CREATED, not when it is rendered
function addTask(text) {
setTasks([{ id: crypto.randomUUID(), text, done: false }, ...tasks]);
} - A database id, an order number, a roll number — the best keys
- An email address or a slug, if it is genuinely unique in the list
- A composite such as
`${row}-${col}`for grids - An id generated once when the item is created and stored with it
- Never
Math.random()orDate.now()in the render — a new key every render forces React to destroy and rebuild the entire list, every time - The array index — only for a static list whose items hold no state
crypto.randomUUID()is built into modern browsers, but it is only available in a secure context — that meanshttps://pages andlocalhost. On a plainhttp://address it will be undefined, so for other environments use a small counter or theuuidpackage instead.
Where the key Goes, and Other Placement Rules
The key belongs on the outermost element returned by the map callback — the thing being repeated — and nowhere else. Putting it on an element inside the callback does not count, and React will keep warning. Putting it on a component in ordinary, non-repeated JSX does nothing useful.
If you are rendering a component, the key goes on the component tag in the parent's map, not inside the component's own JSX. This trips people up because it feels like the component should own its identity. It does not — identity is decided by the parent that places it among its siblings.
A special case: when each item needs to produce several sibling elements without a wrapper, such as a <dt> and a <dd>, you need a keyed Fragment. The shorthand <> cannot take a key, so import Fragment and use the long form.
Lastly, key is reserved. React consumes it and does not pass it through to your component, so reading props.key inside gives undefined. If the component needs the id as well — and it usually does, for delete and edit handlers — pass it a second time under its own name.
import { Fragment } from 'react';
// Key on the outermost repeated element
{products.map(p => (
<ProductCard key={p.id} id={p.id} product={p} />
))}
// ^ for React ^ for your component to use
// WRONG — key is not on the repeated element
{products.map(p => (
<div>
<ProductCard key={p.id} product={p} />
</div>
))}
// Several siblings per item: a keyed Fragment
{entries.map(entry => (
<Fragment key={entry.id}>
<dt>{entry.term}</dt>
<dd>{entry.meaning}</dd>
</Fragment>
))}
// Inside ProductCard, props.key is undefined — that is by design
function ProductCard({ id, product }) {
return <div onClick={() => open(id)}>{product.name}</div>;
} - You can also use
keydeliberately as a reset switch. Giving a component a differentkeymakes React treat it as a new component and rebuild it with fresh state — a neat way to clear a form when the selected record changes:<EditForm key={selectedId} record={record} />.
Filtering, Sorting and Empty States
Lists are rarely rendered raw. You filter by a search box, sort by price or date, maybe slice for pagination. All of that is derived data, so it is calculated during render from state — never stored in a second piece of state, for the reasons the state lesson set out.
Chaining reads well as long as you remember which array methods mutate. filter, map and slice all return new arrays and are safe. sort and reverse change the array in place, so sorting a list that came in as a prop or lives in state will corrupt it — copy first with the spread operator.
Then handle the empty result. A search that matches nothing should say so; a blank area under a search box looks like a broken page. Notice that the empty state depends on the filtered length, not the original — a page that has data but no matches needs a different message from one that has no data at all.
function ProductList({ products }) {
const [query, setQuery] = useState('');
const [sortBy, setSortBy] = useState('name');
// Derived during render — nothing extra stored
const visible = [...products] // copy before sorting
.filter(p => p.name.toLowerCase().includes(query.toLowerCase()))
.sort((a, b) => (sortBy === 'price' ? a.price - b.price : a.name.localeCompare(b.name)));
return (
<div>
<input value={query} onChange={e => setQuery(e.target.value)} placeholder="Search" />
<select value={sortBy} onChange={e => setSortBy(e.target.value)}>
<option value="name">Name</option>
<option value="price">Price</option>
</select>
{products.length === 0 ? (
<p>No products yet.</p>
) : visible.length === 0 ? (
<p>Nothing matches “{query}”.</p>
) : (
<ul>
{visible.map(p => (
<li key={p.id}>{p.name} — ₹{p.price}</li>
))}
</ul>
)}
</div>
);
} - Sorting and filtering a list of a few hundred items on every keystroke is not a performance problem — this runs in well under a millisecond. Only reach for
useMemowhen you have measured a real delay, which in practice means thousands of items or genuinely heavy work per item.
