Introduction to Lists and Keys in React
Many React applications display collections of data, including products, courses, notifications, comments, and search results. Instead of writing individual JSX elements for every item, React allows you to generate elements dynamically from JavaScript arrays.
The JavaScript map() method is commonly used to transform an array of data into React elements. When rendering these elements, React also requires a special attribute called key to identify each item within its collection.
Understanding lists and keys is important because keys influence how React matches elements between renders, preserves component state, and handles changes such as inserting, deleting, and reordering items.
Rendering a List with map()
The map() method creates a new array by applying a function to every element of an existing array. In React, the returned values can be JSX elements.
function TechnologyList() {
const technologies = ['React', 'Angular', 'Vue', 'Svelte'];
return (
<ul>
{technologies.map((technology) => (
<li key={technology}>
{technology}
</li>
))}
</ul>
);
}
export default TechnologyList;
React evaluates the map() expression and renders one list item for every technology. Each item receives a key based on its value. This is suitable here because the technology names are unique and stable within the list.
For arrays containing duplicate values, use a different stable identifier instead of the value itself.
Rendering Arrays of Objects
Real applications commonly receive arrays of objects from APIs or databases. Each object can contain multiple properties, such as an ID, name, description, or price.
const courses = [
{ id: 101, title: 'React Fundamentals', level: 'Beginner' },
{ id: 102, title: 'TypeScript Essentials', level: 'Intermediate' },
{ id: 103, title: 'Advanced JavaScript', level: 'Advanced' }
];
function CourseList() {
return (
<div>
{courses.map((course) => (
<article key={course.id}>
<h3>{course.title}</h3>
<p>Level: {course.level}</p>
</article>
))}
</div>
);
}
export default CourseList;
The course.id property is used as the key because it uniquely identifies each course. Database-generated IDs are generally suitable when they remain unchanged throughout the item's lifetime.
What Are Keys in React?
A key is a special React attribute that identifies an element among its siblings. React uses keys while reconciling the previous render with the next render.
When items are added, removed, or reordered, stable keys help React associate the correct component instances with the corresponding data. This is especially important when list items contain local state, inputs, or other interactive elements.
const users = [
{ id: 'u1', name: 'Asha' },
{ id: 'u2', name: 'Rahul' },
{ id: 'u3', name: 'Meera' }
];
function UserList() {
return (
<ul>
{users.map((user) => (
<li key={user.id}>
{user.name}
</li>
))}
</ul>
);
}
Keys must be unique among siblings in the same list. They do not need to be globally unique across the entire application, so separate lists may reuse the same key values.
Why Stable Keys Matter
Imagine a list of editable tasks. If a new task is inserted at the beginning, the positions of existing tasks change. When React uses stable IDs, it can continue associating each task with the correct component instance.
If array positions are used as keys, React may instead associate existing component state with a different task after the insertion or reordering.
| Key strategy | Behavior | Recommendation |
|---|---|---|
| Stable database ID | Remains associated with the same item. | Preferred for database records. |
| Stable unique string | Identifies the same item across renders. | Suitable for fixed or locally created data. |
| Array index | Can associate state with the wrong item when order changes. | Use only for truly static lists without stable IDs. |
| Random value during render | Changes identity between renders and can repeatedly remount components. | Avoid. |
Why Array Indexes Are Often Poor Keys
The map() callback provides an index, but that index represents the item's current position rather than its identity.
Not recommended for changing lists
{tasks.map((task, index) => (
<TaskItem key={index} task={task} />
))}
Recommended
{tasks.map((task) => (
<TaskItem key={task.id} task={task} />
))}
Index keys are especially problematic when items can be inserted, removed, sorted, or filtered. They may cause input values, focus, or component state to appear associated with the wrong item.
An index can be acceptable for a fixed list that never changes order, never gains or loses entries, and has no stable identifier. Even then, a meaningful stable key is preferable when available.
Filtering Lists Before Rendering
The filter() method creates a new array containing only elements that satisfy a condition. It can be combined with map() to display a subset of data.
const courses = [
{ id: 1, title: 'HTML Basics', published: true },
{ id: 2, title: 'React Hooks', published: false },
{ id: 3, title: 'JavaScript Fundamentals', published: true }
];
function PublishedCourses() {
const publishedCourses = courses.filter(
(course) => course.published
);
return (
<ul>
{publishedCourses.map((course) => (
<li key={course.id}>{course.title}</li>
))}
</ul>
);
}
Filtering does not modify the original array. Each filtered item retains its original ID, so the same key remains valid even when the displayed subset changes.
You can also combine filtering and mapping in a single expression, but separating them into named variables is often easier to read when the logic becomes more complex.
Rendering an Empty List
An empty array produces no elements when mapped. In most applications, it is helpful to display a clear message rather than leaving an unexplained blank area.
function SearchResults({ results }) {
if (results.length === 0) {
return <p>No matching results found.</p>;
}
return (
<ul>
{results.map((result) => (
<li key={result.id}>
{result.title}
</li>
))}
</ul>
);
}
This example assumes results is an array. For asynchronous data, distinguish the loading state from a successfully loaded empty result. Otherwise, users might see a misleading empty-state message while the request is still running.
For more conditional UI patterns, read React Conditional Rendering.
Rendering Lists with Reusable Components
When list items contain several elements or interactive behavior, extracting them into reusable components keeps the parent component focused on managing the collection.
function CourseCard({ course }) {
return (
<article>
<h3>{course.title}</h3>
<p>{course.description}</p>
</article>
);
}
function CourseGrid({ courses }) {
return (
<div className="course-grid">
{courses.map((course) => (
<CourseCard
key={course.id}
course={course}
/>
))}
</div>
);
}
The key belongs on the CourseCard element created inside map(), not on the root article inside CourseCard.
React uses the key internally when matching siblings. The child component does not receive key as an ordinary prop. If the component needs its ID, pass it separately, such as courseId={course.id}.
Learn more about component composition in React Components and passing data in React Props.
Using Fragments in Lists
Sometimes one array item needs to render multiple sibling elements without introducing an additional HTML wrapper. React Fragments can group those elements.
The shorthand Fragment syntax cannot receive a key. When rendering multiple elements from a list, use the explicit Fragment component.
import { Fragment } from 'react';
function DefinitionList({ terms }) {
return (
<dl>
{terms.map((term) => (
<Fragment key={term.id}>
<dt>{term.name}</dt>
<dd>{term.description}</dd>
</Fragment>
))}
</dl>
);
}
export default DefinitionList;
The Fragment provides a stable identity for the group without creating an extra DOM element. This is useful when additional wrappers would interfere with semantic HTML or layout requirements.
Updating Lists with React State
Lists frequently change in response to user interactions. React state can store the collection, while immutable array operations create the updated values.
Avoid directly mutating a state array using operations such as push(), splice(), or sort(). Instead, create a new array and pass it to the state setter.
Adding an item
setTasks((currentTasks) => [
...currentTasks,
{ id: crypto.randomUUID(), title: 'New task' }
]);
Removing an item
setTasks((currentTasks) =>
currentTasks.filter((task) => task.id !== taskId)
);
Updating an item
setTasks((currentTasks) =>
currentTasks.map((task) =>
task.id === taskId
? { ...task, completed: true }
: task
)
);
Each operation returns a new array without changing the previous state. The examples also preserve existing item IDs, allowing React to maintain stable identities across updates.
The crypto.randomUUID() example generates an ID when the new item is created, not during rendering. It requires a supported environment and a secure context. A server-provided ID or another stable ID generator is also appropriate.
See React State for more information about immutable updates.
Sorting Lists Without Mutating State
JavaScript's sort() method modifies the array on which it is called. If the array comes from React state or props, create a copy before sorting.
function SortedCourses({ courses }) {
const sortedCourses = [...courses].sort((a, b) =>
a.title.localeCompare(b.title)
);
return (
<ul>
{sortedCourses.map((course) => (
<li key={course.id}>{course.title}</li>
))}
</ul>
);
}
The spread operator creates a new array before sorting, leaving the original array unchanged. Stable IDs remain associated with the same courses even when their positions change.
For expensive filtering or sorting of large collections, useMemo can sometimes help avoid unnecessary recalculation when dependencies have not changed. Measure the cost before introducing memoization.
Explore React useMemo and React Performance for optimization techniques.
Keys and Component State Preservation
React associates component state with its position and identity in the rendered tree. Keys help determine that identity when siblings change.
Consider a list of task components where each task has an editable input. If the tasks are reordered, stable keys help the input state remain associated with the intended task.
function TaskList({ tasks }) {
return (
<div>
{tasks.map((task) => (
<TaskEditor
key={task.id}
task={task}
/>
))}
</div>
);
}
Conversely, intentionally changing a component's key tells React to treat it as a different component and reset its local state. This can be useful for resetting a form when switching between records, but should be done deliberately.
function ProfileEditor({ selectedUser }) {
return (
<EditProfileForm
key={selectedUser.id}
user={selectedUser}
/>
);
}
When selectedUser.id changes, React creates a fresh instance of the form instead of preserving the previous user's local form state.
Common Mistakes with Lists and Keys
Missing the key attribute
Elements created by map() should have stable keys. Missing keys can produce development warnings and make identity tracking less reliable.
Using duplicate keys
Duplicate keys among siblings can cause incorrect matching and unsupported rendering behavior. Use an identifier that is unique within the collection.
Generating keys during rendering
Avoid key={Math.random()} or key={crypto.randomUUID()} inside a render loop. Changing keys on every render forces React to recreate the corresponding components and can discard local state.
Using array indexes for dynamic data
Index keys can associate state with the wrong item when the collection is reordered, filtered, or modified. Prefer stable IDs.
Mutating state arrays
Do not directly change arrays stored in state. Use operations that produce new arrays and new objects for modified items.
Putting the key in the wrong component
Place the key on the element returned by the list mapping operation, even when that element is a custom component.
Treating keys as normal props
The key attribute is reserved for React and is not passed to the component as a normal prop. Pass a separate ID prop when the child needs it.
Best Practices for React Lists
- Use
map()to transform arrays into JSX elements. - Choose stable, unique keys from the underlying data whenever possible.
- Keep keys consistent when items are filtered, sorted, inserted, or removed.
- Use
filter()andmap()to create updated arrays without mutating state. - Provide meaningful empty-state messages when a collection contains no items.
- Extract complex list items into reusable components.
- Use explicit keyed Fragments when one item needs multiple sibling elements.
- Generate identifiers when data is created, not while rendering it.
- Use semantic HTML such as
ul,ol, andlifor actual lists. - Optimize expensive list operations only when performance measurements justify it.
Key Takeaway
React lists transform JavaScript arrays into reusable UI elements, while keys provide stable identities for those elements. Using unique IDs, immutable array operations, and appropriate empty-state handling makes dynamic collections easier to maintain and helps React preserve the correct component state when data changes.