Runnable lesson · press Try it on the examples to edit and run them here.
A list with zero items is a normal situation: a new user has no orders, a search finds nothing, a filter hides everything. If you ignore it, you show a blank area and the user wonders if the page is broken. A good empty state says what happened and what to do next.
The basic pattern
Check the length before you map:
function App() {
const [tasks, setTasks] = React.useState([]);
return (
<div>
{tasks.length === 0 ? (
<p>You have no tasks yet.</p>
) : (
<ul>
{tasks.map((task) => (
<li key={task.id}>{task.text}</li>
))}
</ul>
)}
<button onClick={() => setTasks([...tasks, { id: tasks.length + 1, text: "Task " + (tasks.length + 1) }])}>
Add a task
</button>
</div>
);
}
An early return keeps JSX simple
When the empty case is the whole component, return early:
function ProductList({ products }) {
if (products.length === 0) {
return <p>No products found.</p>;
}
return (
<ul>
{products.map((p) => <li key={p.id}>{p.name}</li>)}
</ul>
);
}
function App() {
return <ProductList products={[]} />;
}
Different reasons for “nothing”
“Nothing to show” has several causes, and each deserves its own message:
| Situation | Better message |
|---|---|
| The user has never added anything | “No tasks yet. Add your first task.” with a button |
| A search matched nothing | “No results for “xyz”. Try another word.” |
| A filter hides everything | “No items match these filters.” with a Clear filters button |
| Data is still loading | A loading indicator, not “No results” (see Loading States) |
| The request failed | An error message with Retry (see Error States) |
function App() {
const people = ["Ada", "Grace", "Linus", "Margaret"];
const [query, setQuery] = React.useState("");
const matches = people.filter((p) => p.toLowerCase().includes(query.toLowerCase()));
return (
<div>
<input aria-label="Search" placeholder="Type to search" value={query} onChange={(e) => setQuery(e.target.value)} />
{matches.length === 0 ? (
<p>No results for "{query}". Try another word.</p>
) : (
<ul>
{matches.map((p) => <li key={p}>{p}</li>)}
</ul>
)}
</div>
);
}
Hide headers and wrappers with the list
If a list has a heading or a container that looks odd when empty, show it only when there is something in it:
function Comments({ list }) {
if (list.length === 0) return null; // nothing at all, including no heading
return (
<section>
<h3>Comments ({list.length})</h3>
<ul>
{list.map((c) => <li key={c.id}>{c.text}</li>)}
</ul>
</section>
);
}
function App() {
return (
<div>
<Comments list={[]} />
<Comments list={[{ id: 1, text: "Nice!" }, { id: 2, text: "Thanks" }]} />
</div>
);
}
Always provide a safe value
A component that does items.map(...) crashes if items is undefined, which is common before data has arrived. Give it a default (items = []) or guard with items?.length. See Default Prop Values.
Accessibility
Screen readers do not see a blank space. A visible text message works for them as well. If the empty state appears after an action, such as filtering, consider announcing it with role="status" so assistive technology reads it.
Key points
- Check for zero items before rendering and show a helpful message.
- Distinguish “never had any”, “no matches”, “loading” and “failed”.
- Return
nullor hide wrappers and headings when there is nothing to list.