List and List Item Patterns in React
Six components, eight combinations, zero duplication. Here's how separating list containers from list items turns a maintenance headache into a composable system.
The split screen pattern showed that separating "where" from "what" unlocks reuse. Lists have the same problem, just harder to see at first.
I used to write one list component per data type. A UserList that maps over users. A ProductList that maps over products. Each one doing the same loop. When the design needed a compact view and a detailed view of the same data, I ended up with four components instead of two. All of them doing slightly different versions of the same thing.
The fix is applying layout component thinking to lists: separate the list container from the item that renders each row.
Two Variants for Each Item
Start by creating multiple display variants for each item type. Each variant receives only its data object.
const CompactUserRow = ({ user }) => {
const { name, role } = user;
return <p>{name} - {role}</p>;
};
const DetailedUserRow = ({ user }) => {
const { name, role, email, teams } = user;
return (
<>
<h2>{name}</h2>
<p>Role: {role}</p>
<p>Email: {email}</p>
<ul>
{teams.map(team => <li key={team}>{team}</li>)}
</ul>
</>
);
};No styles attached to these components. The parent decides how they look. That keeps each item portable. The same CompactUserRow can appear in a dropdown, a modal preview, or a numbered list without touching the component.
The List Container
The list container is a general component that takes three things: the data array, a string for the prop name, and whatever component renders each row.
const RegularList = ({ items, sourceName, ItemComponent }) => {
return (
<>
{items.map((item, i) => (
<ItemComponent key={i} {...{ [sourceName]: item }} />
))}
</>
);
};The { [sourceName]: item } spread is the key move. sourceName is a computed property key. If sourceName is "user", the spread becomes user={item}. If it is "product", it becomes product={item}. The list container passes the right prop name without knowing what that name is.
RegularList knows nothing about users or products. It only knows how to map items and hand each one to a component.
Using It
<RegularList
items={users}
sourceName="user"
ItemComponent={CompactUserRow}
/>
<RegularList
items={products}
sourceName="product"
ItemComponent={DetailedProductRow}
/>Two lists, two data types, two display variants. Same component.
Adding a Numbered Variant
The NumberedList uses the same interface. It adds an index number before each item.
const NumberedList = ({ items, sourceName, ItemComponent }) => {
return (
<>
{items.map((item, i) => (
<Fragment key={i}>
<h3>{i + 1}</h3>
<ItemComponent {...{ [sourceName]: item }} />
</Fragment>
))}
</>
);
};Swapping RegularList for NumberedList at the call site adds numbers to every row. Nothing inside any item component changes.
With two item variants per data type and two list containers, you get eight combinations from six components. No duplication, no modification needed when you add a third list style. That is what React composition actually means in practice.
In the next post, modals get the same treatment: a layout component that wraps any content and controls its own visibility.
The Essentials
- Item components should receive only their domain object and contain no layout styles.
{ [sourceName]: item }is a computed property key spread. It lets the list container pass any prop name the item component expects.- The list container accepts
ItemComponentas a prop, making it work with any display variant. - A
NumberedListis the same interface asRegularListwith index rendering added. - Six components produces eight display combinations through composition, not duplication.
Further Reading and Watching
- Master React Design Patterns (render prop and HOC) by CoderOne. Covers how composition patterns connect to more advanced React patterns.
- React docs: Rendering Lists covers the fundamentals of
keyprops and why they matter when mapping data.
Practice what you just read.
Keep reading