2 · Build one skill at a time · 18 MIN
Render a list and an empty state
Map data to elements and give each item a stable identity.
map creates one element per data item. A key tells React which item is which across additions, removals and reordering. Use the data’s stable id rather than a random value or a changing position. filter first selects the items to display; it does not mutate the original array. When nothing matches, render a useful sentence instead of an unexplained blank area. The search result is calculated from the query, so it does not need separate state or an effect.
Read the original data outside the component. Follow query through filter before map renders it. Type an unmatched term and inspect the conditional branch. Normalize the query once with trim().toLowerCase().
Before you start
Install Node.js 22.12+ (or a supported newer LTS). In a terminal run: npm create vite@latest codingneed-react -- --template react. Then cd codingneed-react, npm install, and npm run dev. Open the local URL printed by Vite. Replace src/App.jsx with ONE lesson example at a time. JSX renders a browser interface; it is not a plain console script.
File for this example: App.jsx
New words, explained
- map
- Transform each array item into another value.
- key
- A stable identifier among sibling elements.
- derived
- Calculated from existing inputs instead of stored twice.
Follow the example step by step
- Read the original data outside the component.
- Follow query through filter before map renders it.
- Type an unmatched term and inspect the conditional branch.
- Normalize the query once with trim().toLowerCase().
You are ready to move on when: Search is case insensitive. Searching spaces around SQL finds SQL. No-match and clear-search states both work.
Read the example
import {useState} from 'react';
const topics = [{id:'r', title:'React'}, {id:'s', title:'SQL'}, {id:'n', title:'Node.js'}];
export default function App() {
const [query, setQuery] = useState('');
const visible = topics.filter(topic => topic.title.toLowerCase().includes(query.toLowerCase()));
return <main><h1>Find a topic</h1><label htmlFor="query">Search</label>
<input id="query" value={query} onChange={event => setQuery(event.target.value)} />
{visible.length ? <ul>{visible.map(topic => <li key={topic.id}>{topic.title}</li>)}</ul> : <p>No topics match. Clear the search to try again.</p>}
</main>;
}Check the expected output
All three topics appear initially. Searching sql shows SQL. Searching xyz shows the empty-state message.
Your challenge
Add Python with a stable id and make search ignore leading and trailing spaces.
Solution cost: Discuss the operations in this small example; rendering and I/O costs depend on the host. time · Proportional to the example’s retained data. space
Common trap
Random keys change identity on every render and can reset child state.
Study the project implementation
import {useState} from 'react';
const topics = [{id:'r', title:'React'}, {id:'s', title:'SQL'}, {id:'n', title:'Node.js'}];
export default function App() {
const [query, setQuery] = useState('');
const visible = topics.filter(topic => topic.title.toLowerCase().includes(query.toLowerCase()));
return <main><h1>Find a topic</h1><label htmlFor="query">Search</label>
<input id="query" value={query} onChange={event => setQuery(event.target.value)} />
{visible.length ? <ul>{visible.map(topic => <li key={topic.id}>{topic.title}</li>)}</ul> : <p>No topics match. Clear the search to try again.</p>}
</main>;
}Further reading: Official documentation
Next lesson: Share state between two components →