3 · Reliability and delivery · 30 MIN
Derived state and searchable lists
Values calculated from existing state usually belong in rendering.
The visible list depends entirely on the query and the source courses, so it can be computed during rendering. Keeping a second visibleCourses state and synchronizing it with an effect creates another value that can fall out of date. This small list does not need useMemo; measure before adding memoization. For larger catalogs, consider server search, pagination or virtualization as appropriate. Derived values should preserve the same matching rules across the count, list and empty state.
Check case-insensitive and trimmed search.
Read the example
import {useState} from 'react';
const courses=[{id:'js',title:'JavaScript'},{id:'py',title:'Python'},{id:'sql',title:'SQL'}];
export default function App() {
const [query,setQuery]=useState('');
const visible=courses.filter(c=>c.title.toLowerCase().includes(query.trim().toLowerCase()));
return <main><h1>Find a course</h1><label>Search<input value={query} onChange={e=>setQuery(e.target.value)}/></label>
<p role="status">{visible.length} matches</p><ul>{visible.map(c=><li key={c.id}>{c.title}</li>)}</ul>
{!visible.length&&<p>Try another search.</p>}</main>;
}Check the expected output
An empty query shows three courses; PY shows Python; xyz shows zero matches and an empty-state message.
Your challenge
Add a topic filter and derive the displayed count and list from both inputs without adding synchronization effects.
Solution cost: O(n × m) simple substring search across n titles of length up to m. time · O(n) matching references. space
Common trap
Effects used only to synchronize redundant state introduce unnecessary renders and stale intermediate values.
Study the project implementation
import {useState} from 'react';
const courses=[{id:'js',title:'JavaScript'},{id:'py',title:'Python'},{id:'sql',title:'SQL'}];
export default function App() {
const [query,setQuery]=useState('');
const visible=courses.filter(c=>c.title.toLowerCase().includes(query.trim().toLowerCase()));
return <main><h1>Find a course</h1><label>Search<input value={query} onChange={e=>setQuery(e.target.value)}/></label>
<p role="status">{visible.length} matches</p><ul>{visible.map(c=><li key={c.id}>{c.title}</li>)}</ul>
{!visible.length&&<p>Try another search.</p>}</main>;
}Further reading: Official documentation
Next lesson: Project · A timer with reliable cleanup →