If a value can be calculated from current props and state during rendering, storing another copy in state often creates synchronization work. A filtered list is a common example. You need state for the search input, but the matching rows can usually be derived directly from that input and the source list.
This component treats courses as the source of truth and query as the editable state. It normalizes the comparison without changing the original course titles. Every render computes the visible rows from the current inputs, so a new courses prop does not need a separate synchronization effect.
import { useState } from "react";
export default function CourseSearch({ courses }) {
const [query, setQuery] = useState("");
const normalized = query.trim().toLowerCase();
const visible = courses.filter(course =>
course.title.toLowerCase().includes(normalized)
);
return (
<section>
<label htmlFor="course-search">Find a course</label>
<input id="course-search" value={query}
onChange={event => setQuery(event.target.value)} />
<p>{visible.length} matching courses</p>
<ul>{visible.map(course =>
<li key={course.id}>{course.title}</li>
)}</ul>
</section>
);
}A pattern that stores filteredCourses and updates it in an effect introduces another value that must remain consistent. The component first renders, then the effect runs and triggers another update. It is easier to forget a dependency or display a briefly stale result. Direct derivation avoids that redundant state.
For a genuinely expensive calculation, useMemo can reuse a result until its dependencies change. Measure before adding it to a small filter. It does not fix a mutated source array or an incorrect filter rule, and correctness should not depend on the cache surviving forever.
Use a stable course identifier as the key rather than the displayed index. Do not sort or splice the incoming courses array in place. If sorting is part of the derived view, work on a copy so other consumers do not observe an unexpected mutation.
Add a beginner-only checkbox and derive the visible rows from both controls. Test an empty query, mixed-case titles and a parent update that replaces the course list. Do not introduce separate state for the filtered result.
Explore the related Code Hacker course syllabus for structured learning. Then read Prevent Stale Search Results with AbortController to connect this exercise to the next skill.
React: You Might Not Need an Effect provides the underlying API or technical guidance. Check the documentation for the version you use; examples here are small learning exercises rather than complete production implementations.
Found an issue with this guide? Send a correction with the article URL and your example. About our learning material.
Subscribe to Code Hacker and get hands-on access to 40+ premium courses — Ethical Hacking, Kali Linux, Metasploit, Network Hacking, Bug Bounty & more!