React Filtering Lists

In the last lesson you learned React Dynamic List Rendering, where you turned an array into elements with map. This lesson shows how to display only the items that match what the user wants, which is called filtering.

🤔 Why do we need filtering?

Most real screens don’t show everything in the list. They show a slice of it based on what the user wants right now.

  • A music app shows your whole library, but the search box narrows it to songs that match your text.
  • A to-do app has tabs like All, Active, and Done. Each tab shows only some of the tasks.
  • An online store has a “in stock only” toggle that hides the sold-out items.

So you have one full list, and on screen you show only the items that pass a test. That test is the filter.

🧩 The pattern: filter, then map

The whole idea is two array methods working back to back:

  • filter walks the list and keeps only the items that pass your test, so it gives back a smaller array.
  • map then takes that smaller array and turns each item into JSX.
  • You chain them like items.filter(...).map(...), so filter goes first and map goes second.

Here we keep only the active tasks, then render each one as a list item.

function TaskList({ tasks }) {
return (
<ul>
{tasks
.filter((task) => !task.done) // keep only the unfinished ones
.map((task) => <li key={task.id}>{task.text}</li>)}
</ul>
);
}

So filter throws away the finished tasks, and map runs only on the ones that survived. The order matters, so shrink the list first, then draw what’s left.

Now let’s make it react to typing. We keep the search text in state, and we filter the list by that text every time the user types.

import { useState } from "react";
function ProductSearch() {
const [query, setQuery] = useState("");
const products = ["Phone", "Laptop", "Headphones", "Camera", "Charger"];
const visible = products.filter((p) =>
p.toLowerCase().includes(query.toLowerCase())
);
return (
<div>
<input
value={query}
placeholder="Search products"
onChange={(e) => setQuery(e.target.value)}
/>
<ul>
{visible.map((p) => <li key={p}>{p}</li>)}
</ul>
</div>
);
}

Let’s read it from the top:

  • query holds whatever the user has typed so far, and it starts as an empty string.
  • The input is controlled, so its value is query and every keystroke calls setQuery with the new text.
  • visible is the filtered list, so we keep a product only if its name contains the query text.
  • We call toLowerCase() on both sides so the match ignores capital letters, which means “lap” finds “Laptop” too.

So when you type “ph”, the filter keeps “Phone” and “Headphones”, and the list shows just those two. This is case-insensitive matching, which is what users expect from a search box.

includes is your search test

text.includes(query) is true when query appears anywhere inside text. Lowercasing both sides first means “PHONE”, “phone”, and “Phone” all match the same way.

🔘 Filter buttons (all / active / completed)

Search filters by text. Buttons filter by category. The setup is the same idea: keep the chosen filter in state, then decide what to show based on it.

import { useState } from "react";
function TodoApp() {
const [filter, setFilter] = useState("all");
const tasks = [
{ id: 1, text: "Buy milk", done: true },
{ id: 2, text: "Walk dog", done: false },
{ id: 3, text: "Read book", done: false },
];
const visible = tasks.filter((task) => {
if (filter === "active") return !task.done;
if (filter === "completed") return task.done;
return true; // "all" keeps everything
});
return (
<div>
<button onClick={() => setFilter("all")}>All</button>
<button onClick={() => setFilter("active")}>Active</button>
<button onClick={() => setFilter("completed")}>Completed</button>
<ul>
{visible.map((task) => <li key={task.id}>{task.text}</li>)}
</ul>
</div>
);
}

Here is what each part does:

  • The filter state holds the current choice as a string, so “all”, “active”, or “completed”.
  • Each button just calls setFilter with its own value when clicked.
  • The filter test reads that state, so “active” keeps unfinished tasks, “completed” keeps finished ones, and “all” returns true so nothing gets dropped.

So clicking a button changes one small string in state, React re-renders, and the list filters itself to match. The thing you store is the filter choice, not the filtered list.

Store the choice, not the result

Notice the state is just the word “active”. It is not the filtered array of tasks. That tiny difference is the whole point of the next section.

💡 Don’t store the filtered list in state

This is the most important habit in the whole lesson:

  • You already have the source list, and you already have the filter value in state.
  • The filtered list is just those two combined, so it is derived, not separate.
  • If you copy it into its own state, you now have two things that must stay in sync by hand, and they will drift apart.

Here is the trap. We store the search results in their own state, and then forget to update them.

// ❌ Wrong - results live in state and go stale
const [query, setQuery] = useState("");
const [results, setResults] = useState(products);
function handleChange(e) {
setQuery(e.target.value);
// forgot to recompute results... now the list never updates
}
// ✅ Right - compute the filtered list during render
const [query, setQuery] = useState("");
const results = products.filter((p) =>
p.toLowerCase().includes(query.toLowerCase())
);
// runs fresh on every render, so it can never be out of date

Look at the difference:

  • In the wrong version, results is its own state, so when the query changes you have to remember to recompute and set it. Miss that one step and the screen shows old results forever.
  • In the right version, results is a plain const worked out during render, so every render rebuilds it from the current query. There is nothing to keep in sync.

So the derived value is calculated fresh each render. The source list and the query are the truth, and the filtered list is just a view of them, so it does not deserve its own state.

Derived data does not belong in state

A good test: can I calculate this value from other state or props? If yes, don’t put it in state. Compute it during render instead. State you can derive is state that will go stale.

🖥️ Search and buttons together

Real apps often combine both. Let’s put a search box and filter buttons in one component, and notice that we still store no filtered list. Just the query and the filter choice.

import { useState } from "react";
function TaskBoard() {
const [query, setQuery] = useState("");
const [filter, setFilter] = useState("all");
const tasks = [
{ id: 1, text: "Buy milk", done: true },
{ id: 2, text: "Walk dog", done: false },
{ id: 3, text: "Read book", done: false },
];
const visible = tasks
.filter((task) => {
if (filter === "active") return !task.done;
if (filter === "completed") return task.done;
return true;
})
.filter((task) => task.text.toLowerCase().includes(query.toLowerCase()));
return (
<div>
<input
value={query}
placeholder="Search tasks"
onChange={(e) => setQuery(e.target.value)}
/>
<button onClick={() => setFilter("all")}>All</button>
<button onClick={() => setFilter("active")}>Active</button>
<button onClick={() => setFilter("completed")}>Completed</button>
<ul>
{visible.map((task) => <li key={task.id}>{task.text}</li>)}
</ul>
</div>
);
}

So the two filters chain together:

  • The first filter narrows by the button choice, then the second filter narrows by the search text.
  • Then map renders whatever is left.
  • We store only query and filter, so visible is derived from them on every render. One source of truth, no stale copies.

Output

filter = "active", query = "wa"
Shows: Walk dog
filter = "all", query = ""
Shows: Buy milk, Walk dog, Read book

✅ Best Practices

A few habits that keep filtered lists clean and bug-free.

  • Keep the source list and the filter value in state. Never the filtered result.
  • Derive the filtered list with a const during render so it is always fresh.
  • Chain filter before map. Shrink the list first, then turn it into JSX.
  • For text search, lowercase both the item and the query before includes so matching ignores case.
  • Always give each rendered item a stable key, the same as any other list you map over.

🧩 What You’ve Learned

  • ✅ Filtering means showing only the items that pass a test, using filter
  • ✅ The core pattern is items.filter(...).map(...) — filter first, then map
  • ✅ A live search keeps the query in state and filters with includes on lowercased text
  • ✅ Filter buttons store the chosen filter as a string in state and switch the test by it
  • ✅ Never store the filtered list in state — it goes stale and needs manual syncing
  • ✅ Derive the filtered list during render from the source list plus the filter value
  • ✅ This keeps a single source of truth, the same best practice you use everywhere in state

Check Your Knowledge

Test what you learned. Pick an answer for each question, then click Check.

  1. 1

    What is the correct order when filtering a list and rendering it?

    Why: filter shrinks the list to the items that pass the test, then map turns only those into JSX. Filtering first means map runs on the smaller list.

  2. 2

    How do you make a text search ignore capital letters?

    Why: Lowercasing both sides before includes means 'Phone', 'phone', and 'PHONE' all match the same query, which is what users expect from search.

  3. 3

    Why should you NOT store the filtered list in its own state?

    Why: The filtered list can always be computed from the source list plus the filter value. Keeping it in its own state means two things that must stay in sync, which leads to stale data.

  4. 4

    With filter buttons, what should actually be stored in state?

    Why: Store just the chosen filter, like 'all' or 'active'. The visible list is then derived during render from that choice plus the source list, so it never goes stale.

🚀 What’s Next?

You can now show just the part of a list the user wants, and you do it without ever storing the filtered result. Next we’ll take the same derive-during-render idea and use it to reorder a list instead of shrinking it.

React Sorting Lists

Share & Connect