React Filtering Lists
Table of Contents + −
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:
filterwalks the list and keeps only the items that pass your test, so it gives back a smaller array.mapthen 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.
🔍 A live search box
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:
queryholds whatever the user has typed so far, and it starts as an empty string.- The input is controlled, so its
valueisqueryand every keystroke callssetQuerywith the new text. visibleis 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
filterstate holds the current choice as a string, so “all”, “active”, or “completed”. - Each button just calls
setFilterwith 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 staleconst [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 renderconst [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 dateLook at the difference:
- In the wrong version,
resultsis 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,
resultsis a plainconstworked 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
filternarrows by the button choice, then the secondfilternarrows by the search text. - Then
maprenders whatever is left. - We store only
queryandfilter, sovisibleis 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
constduring render so it is always fresh. - Chain
filterbeforemap. Shrink the list first, then turn it into JSX. - For text search, lowercase both the item and the query before
includesso 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
includeson 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
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
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
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
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.