Student Directory Project: Combine Components, State and Practice
Student Directory Project: Combine Components, State and Practice
A teacher needs to search a class list, change attendance and see an accurate present count. This is small enough to build independently, yet it combines the ownership and update decisions learned throughout the course.
The project is a client-side school directory with an explicit local data fixture. A parent owns students and search input, derives the visible list and present count, and passes focused inputs and callbacks to children. It is a complete local interaction, not a production student-information system.
Understand
Build a working searchable attendance directory.
Build
Keep one owner for data that must agree.
Check
Verify empty search results, toggles and derived counts.
One data owner coordinates search and attendance
Own facts
App stores students and the query.
Derive views
Filter names and count present students from the same facts.
Request changes
Rows call the supplied toggle callback with a stable ID.
Write the requirements before assembling components
The initial list contains Maya absent and Ravi present. An empty search shows both. Searching a name ignores letter case and surrounding whitespace. A no-match query shows a clear message. Toggling a visible student changes that record and the overall present count. Search does not delete students or change attendance.
App owns the records because rows and the summary must agree. StudentRow receives a record and an onToggle callback; it does not duplicate the record in separate state. The query is another independent fact, while the visible list and counts are calculated during rendering. No Effect is needed for those calculations.
Build the complete local directory
The full source uses functions, immutable array updates, controlled input, conditional rendering and stable keys. Each button’s accessible name identifies its student. The count covers the whole class, not just search matches, so filtering cannot silently change the attendance summary.
React · JavaScript with JSX
import { useState } from 'react';
function StudentRow({ student, onToggle }) {
return <li>{student.name}: {student.present ? 'present' : 'absent'} <button onClick={() => onToggle(student.id)}>Toggle {student.name}</button></li>;
}
export default function App() {
const [students, setStudents] = useState([{ id: 1, name: 'Maya', present: false }, { id: 2, name: 'Ravi', present: true }]);
const [query, setQuery] = useState('');
const visible = students.filter(s => s.name.toLowerCase().includes(query.trim().toLowerCase()));
const present = students.filter(s => s.present).length;
function toggle(id) { setStudents(current => current.map(s => s.id === id ? { ...s, present: !s.present } : s)); }
return <main><h1>School directory</h1><p>Local fixture; changes are not saved</p>
<label>Search students<input value={query} onChange={event => setQuery(event.target.value)} /></label>
<p>Present: {present} of {students.length}</p>
{visible.length ? <ul>{visible.map(s => <StudentRow key={s.id} student={s} onToggle={toggle} />)}</ul> : <p>No students match</p>}
</main>;
}Expected screen and behavior
Toggle Maya makes Present: 2 of 2. Searching Ravi shows only Ravi without changing the count. An unmatched query displays No students match.Trace one attendance change and one search
React · Build the complete local directory
import { useState } from 'react';
function StudentRow({ student, onToggle }) {
return <li>{student.name}: {student.present ? 'present' : 'absent'} <button onClick={() => onToggle(student.id)}>Toggle {student.name}</button></li>;
}
export default function App() {
const [students, setStudents] = useState([{ id: 1, name: 'Maya', present: false }, { id: 2, name: 'Ravi', present: true }]);
const [query, setQuery] = useState('');
const visible = students.filter(s => s.name.toLowerCase().includes(query.trim().toLowerCase()));
const present = students.filter(s => s.present).length;
function toggle(id) { setStudents(current => current.map(s => s.id === id ? { ...s, present: !s.present } : s)); }
return <main><h1>School directory</h1><p>Local fixture; changes are not saved</p>
<label>Search students<input value={query} onChange={event => setQuery(event.target.value)} /></label>
<p>Present: {present} of {students.length}</p>
{visible.length ? <ul>{visible.map(s => <StudentRow key={s.id} student={s} onToggle={toggle} />)}</ul> : <p>No students match</p>}
</main>;
}Toggle Maya supplies ID 1. The parent’s functional updater receives the current list and returns a replacement Maya record while retaining Ravi. The next render derives present 2 from that list and describes the new rows.
Entering Ravi changes only query. The same two records still exist; visible now contains Ravi, but the present count still reads all students. This distinguishes a display filter from changing the source data. Clearing the search restores both rows with their updated attendance.
Extend one responsibility at a time
Split the code into files when it improves responsibility boundaries, keeping the shared student owner and row interface explicit. Add a detail route only after deciding how selection appears in the URL. Add real loading and mutation only when an API contract exists; then include failure, authorization and stale-response rules.
For independent practice, first build the local requirements without copying the answer. Explain who owns each value, which values are derived, and why stable IDs matter. Then compare your behavior with the complete example, not just its visual appearance.
Try the school task
React · starting example
import { useState } from 'react';
function StudentRow({ student, onToggle }) {
return <li>{student.name}: {student.present ? 'present' : 'absent'} <button onClick={() => onToggle(student.id)}>Toggle {student.name}</button></li>;
}
export default function App() {
const [students, setStudents] = useState([{ id: 1, name: 'Maya', present: false }, { id: 2, name: 'Ravi', present: true }]);
const [query, setQuery] = useState('');
const visible = students.filter(s => s.name.toLowerCase().includes(query.trim().toLowerCase()));
const present = students.filter(s => s.present).length;
function toggle(id) { setStudents(current => current.map(s => s.id === id ? { ...s, present: !s.present } : s)); }
return <main><h1>School directory</h1><p>Local fixture; changes are not saved</p>
<label>Search students<input value={query} onChange={event => setQuery(event.target.value)} /></label>
<p>Present: {present} of {students.length}</p>
{visible.length ? <ul>{visible.map(s => <StudentRow key={s.id} student={s} onToggle={toggle} />)}</ul> : <p>No students match</p>}
</main>;
}Add a Show absent only checkbox. It should combine with the name search while leaving the overall attendance count based on the whole class.
Worked answer
The checkbox adds one independent boolean. visible applies both conditions, and the summary still derives from students. Testing Maya’s transition to present while absent-only is checked reveals why the row disappears without deleting the record.
React · JavaScript with JSX
import { useState } from 'react';
export default function App() {
const [students, setStudents] = useState([{ id: 1, name: 'Maya', present: false }, { id: 2, name: 'Ravi', present: true }]);
const [query, setQuery] = useState('');
const [absentOnly, setAbsentOnly] = useState(false);
const visible = students.filter(s => s.name.toLowerCase().includes(query.trim().toLowerCase()) && (!absentOnly || !s.present));
const present = students.filter(s => s.present).length;
return <main><h1>Directory filters</h1><label>Search students<input value={query} onChange={event => setQuery(event.target.value)} /></label>
<label><input type="checkbox" checked={absentOnly} onChange={event => setAbsentOnly(event.target.checked)} />Show absent only</label>
<p>Present: {present} of {students.length}</p>
{visible.length ? <ul>{visible.map(s => <li key={s.id}>{s.name}: {s.present ? 'present' : 'absent'} <button onClick={() => setStudents(current => current.map(item => item.id === s.id ? { ...item, present: !item.present } : item))}>Toggle {s.name}</button></li>)}</ul> : <p>No students match</p>}
</main>;
}Expected screen and behavior
Show absent only displays Maya. Toggling Maya removes the only absent row, shows No students match and changes Present to 2 of 2.Mistakes and boundaries
The local fixture is not persistent and does not enforce school permissions. Do not represent it as a completed production student-data service.
Names are not guaranteed unique, so IDs provide identity. A production API should supply stable identities and validate changes rather than trust the visible text.
Verify no matches, empty search, repeated toggles, checkbox interaction and keyboard input. A happy-path screenshot alone does not check those requirements.
Check your reasoning
React · Build the complete local directory
import { useState } from 'react';
function StudentRow({ student, onToggle }) {
return <li>{student.name}: {student.present ? 'present' : 'absent'} <button onClick={() => onToggle(student.id)}>Toggle {student.name}</button></li>;
}
export default function App() {
const [students, setStudents] = useState([{ id: 1, name: 'Maya', present: false }, { id: 2, name: 'Ravi', present: true }]);
const [query, setQuery] = useState('');
const visible = students.filter(s => s.name.toLowerCase().includes(query.trim().toLowerCase()));
const present = students.filter(s => s.present).length;
function toggle(id) { setStudents(current => current.map(s => s.id === id ? { ...s, present: !s.present } : s)); }
return <main><h1>School directory</h1><p>Local fixture; changes are not saved</p>
<label>Search students<input value={query} onChange={event => setQuery(event.target.value)} /></label>
<p>Present: {present} of {students.length}</p>
{visible.length ? <ul>{visible.map(s => <StudentRow key={s.id} student={s} onToggle={toggle} />)}</ul> : <p>No students match</p>}
</main>;
}Choose the answer that follows the data and the interaction, then read the explanation for each alternative.
Carry the idea forward
You can now explain the owner, derived views, update mechanism and useful tests for a small school interface. Revisit state structure, behavior checks and delivery requirements when extending it.
Read the primary reference: React: thinking in React.
Reaching the recap and completing the quizzes are separate milestones.