Build a React Student Registry
Build a typed React student registry that saves fictional records locally.
Introduction
30 Second Summary
A student list grows every time someone joins a class. Losing that list after a browser refresh turns a quick update into repeated work.
In this project, you will build the first usable screen of the Campus Grade Generator with React plus TypeScript. The dashboard keeps fictional student records searchable through each browser refresh.
What You'll Build
Your finished teacher dashboard turns every fictional registration into a searchable table row that remains after a browser refresh.
By the end of this project, you'll have:
- An instant registration flow that turns fictional student details into a new table row immediately.
- A refresh-proof registry that restores saved records from localStorage whenever the page returns.
- A teacher-friendly table that finds students by ID, name, or class section. Each Delete action removes the same record from browser storage.
- Secret Mission: Add a class-section filter that works together with search.
Are there any prerequisites?
You need a Windows computer with Node.js, Visual Studio Code, plus Microsoft Edge or Chrome installed. No React or TypeScript experience is required beyond basic comfort with files and folders.
Before We Start
This checkpoint locks in the single-device Stage 1 scope: a React and TypeScript student registry using fictional records to keep real student information out of the unauthenticated demo. Each student ID becomes the shared link for later QR codes, assessments, grades, reports, and backups.
Scaffold and Open the React App
The student registry needs a dependable place to run before you add forms or records. A working starter page proves your browser can reach your local development tools.
Vite supplies an official React and TypeScript template that avoids manual build configuration. You will run that template in your browser before opening its files in Visual Studio Code.
In this step, get ready to:
- Confirm that Node.js can run the current Vite template.
- Scaffold the campus-grade-generator project and install its dependencies.
- Run the starter page and open the generated files in Visual Studio Code.
Check Node.js and scaffold the project
Node.js runs the tools that build and serve your app. Vite requires Node.js 20.19+ or 22.12+.
- Press the Windows key to open Windows search.
- Type Windows PowerShell into the search bar.
- Press Enter to open Windows PowerShell.
- Check your installed Node.js version by running this command:
node --version
What does this command do?
The --version flag asks Node.js to print its installed version. That result determines whether your computer can run the current Vite template.
✔️ I see a compatible version
If the output is v24.22.0, your current LTS version is ready. Other versions that satisfy Vite's listed requirement can continue.
ⓧ I see an older version
Your installed version is below Vite's requirement. Install the current Node.js LTS build before scaffolding the app.
- Open the official Node.js download page in your browser.
- Download the Windows build for Node.js v24.22.0 LTS.
- Run the downloaded installer.
- Keep the default installer options.
- Close the installer after it confirms installation.
- Close Windows PowerShell.
- Reopen Windows PowerShell through Windows search.
- Rerun the version check shown above.
ⓧ Command not found
Windows cannot find a Node.js installation. Install the current LTS build before continuing.
- Open the official Node.js download page in your browser.
- Download the Windows build for Node.js v24.22.0 LTS.
- Run the downloaded installer.
- Keep the default installer options.
- Close the installer after it confirms installation.
- Close Windows PowerShell.
- Reopen Windows PowerShell through Windows search.
- Rerun the version check shown above.
The Vite scaffolding command launches an interactive wizard. Each answer selects the project name and the generated toolchain.
- Move to your Desktop by running this command:
cd ~/Desktop
What does this command do?
PowerShell changes its current location to your Desktop. Vite creates the new campus-grade-generator folder there, which makes it easy to find.
- Use campus-grade-generator when the wizard shows Project name:.
- Select React when the wizard shows Select a framework:.
- Select TypeScript when the wizard shows Select a variant:.
- Keep Oxlint when the wizard shows Which linter to use?.
- Decline the final immediate-start prompt.
- Start the Vite wizard by running this command:
npm create vite@latest
What does this command do?
The npm command downloads the current create-vite scaffolder. Your wizard choices generate the React and TypeScript project files inside campus-grade-generator.
- Confirm that PowerShell reports the campus-grade-generator project was created.
Did the project fail to scaffold?
Check that PowerShell is still open on your Desktop. Confirm that you entered the project name exactly as campus-grade-generator.
If the command reports a Node.js compatibility problem, install the current LTS build using the matching tab above. Reopen PowerShell after the installation finishes.
Help me troubleshoot the Vite scaffolding command in Windows PowerShell.
Install dependencies and run the starter
The scaffold describes the packages your starter app needs in package.json. Installing those packages gives Vite everything required to start the local development server.
- Move into the generated folder and install its dependencies by running these commands:
cd campus-grade-generator
npm install
What do these commands do?
- The first command moves PowerShell into the generated campus-grade-generator folder.
- The second command reads package.json and installs the listed project dependencies.
Dependency installation can take a few minutes while npm downloads the packages. When it finishes, PowerShell returns to the prompt without an installation error.
Did dependency installation fail?
Check that the PowerShell path ends with campus-grade-generator. A different path means npm cannot find this project's package.json.
Confirm that your internet connection is active before retrying the installation.
Help me troubleshoot npm install for this Vite project.
Before you start the server, what do you expect PowerShell to print when the app is ready? Pause for a moment.
- Start the Vite development server by running this command:
npm run dev
What does this command do?
The dev script starts Vite's local development server. PowerShell stays occupied while the server is running because it is waiting for browser requests.
PowerShell now prints a local URL for the running app. The default URL is often http://localhost:5173 when that port is available.
The local URL opens in Microsoft Edge or Google Chrome. Keep the PowerShell development server running while you use the app.
- Copy the local URL printed by Vite in PowerShell.
- Press the Windows key to open Windows search.
- Type Microsoft Edge or Google Chrome into the search bar.
- Press Enter to open the browser.
- Paste the local URL into the browser address bar.
- Press Enter to load the app.
You should see the Vite React starter page. That visible page proves the generated app is running through your local development server.
Can't see the starter page?
Confirm that the PowerShell window running the server is still open. Copy the local URL directly from that window because Vite can choose a different port when its default is busy.
Check that PowerShell started the server from the campus-grade-generator folder.
Help me open the local Vite starter page.
Open the generated files in Visual Studio Code
The generated folder contains the app source plus its Vite and TypeScript configuration. Opening the folder as a workspace keeps those files together in the Explorer.
- Press the Windows key to open Windows search.
- Type Visual Studio Code into the search bar.
- Press Enter to open Visual Studio Code.
- Click File in the top menu bar.
- Select Open Folder....
- Select the campus-grade-generator folder on your Desktop.
- Click Select Folder.
- Select Yes, I trust the authors if the Workspace Trust dialog appears.
The Explorer now shows campus-grade-generator as the open workspace. This confirms Visual Studio Code is using the same folder as the running PowerShell server.
- Confirm that the Explorer lists package.json, vite.config.ts, tsconfig.json, tsconfig.app.json, tsconfig.node.json, and index.html.
- Expand the src folder to confirm that it contains App.tsx, App.css, index.css, and main.tsx.
- Select index.html in the Explorer.
- Replace its contents with the code below:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Campus Grade Generator</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
What does this file do?
- The head section provides browser metadata and sets the tab title to Campus Grade Generator.
- The root element gives React a place to render the app.
- The module script loads src/main.tsx as the app's entry point.
- Save index.html.
Before you refresh the browser, which part of the starter experience do you expect to change? Make your prediction first.
- Switch back to the browser tab showing the local Vite app.
- Refresh the local page.
The Vite React starter page remains visible. The browser tab now reads Campus Grade Generator, which confirms your saved file reached the running app.
Does the browser tab still show the old title?
Confirm that you saved index.html inside the open campus-grade-generator workspace. Check that the browser is using the local URL printed by the running Vite server.
Help me troubleshoot the page title in my Vite app.
✔️ Awesome, I've got everything!
Great work. Keep the PowerShell development server running and leave the campus-grade-generator workspace open in Visual Studio Code.
ⓧ I'd like to double check the full code
Compare these generated files with your project before moving on.
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Campus Grade Generator</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
{
"name": "campus-grade-generator",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "oxlint",
"preview": "vite preview"
},
"dependencies": {
"react": "^19.3.1",
"react-dom": "^19.3.1"
},
"devDependencies": {
"@types/node": "^24.19.2",
"@types/react": "^19.3.0",
"@types/react-dom": "^19.3.0",
"@vitejs/plugin-react": "^6.1.2",
"oxlint": "^1.88.0",
"typescript": "~6.0.3",
"vite": "^8.3.4"
}
}
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(<App />)
Your React and TypeScript starter is live. Next, you will replace the starter content with an in-memory student registration form and registry table.
Build the In-Memory Registry
Your Vite starter is running in the browser. You now have a fast feedback loop for the first registry screen.
A registry needs visible records before browser persistence has anything to save. You will build the in-memory version with React state and TypeScript types. A final refresh test exposes the limit of this first version.
In this step, get ready to:
- Define a reusable student data type.
- Build a controlled registration form and a registry table.
- Add a fictional student to state and observe what happens after a refresh.
Define the student data shape
A TypeScript interface describes the fields every student record must contain. Keeping this shape in its own file lets the form, table, and later project stages share one definition.
- Return to the Explorer sidebar in Visual Studio Code.
- Select the src folder.
- Use the folder creation control at the top of the Explorer sidebar.
- Name the new folder types.
You should now see types inside src.
- Select the types folder.
- Use the file creation control at the top of the Explorer sidebar.
- Name the new file student.ts.
- Define the shared student shape by pasting this code into src/types/student.ts:
export interface Student {
studentId: string
fullName: string
classSection: string
}
What Does This Interface Define?
- The exported Student interface gives other files one reusable record shape.
- The studentId field holds the fictional identifier that later Campus Grade Generator stages can reuse.
- The fullName field holds the fictional student's name.
- The classSection field keeps the student's class grouping separate from their identity.
- Save src/types/student.ts.
- Confirm that the Explorer lists student.ts inside src/types.
Seeing TypeScript Errors?
Check that each property ends with : string. Confirm that the opening and closing braces are both present.
Ask for help with the student interface.
✔️ Awesome, I've got everything!
Great. Your project now has a reusable type for every student record.
ⓧ I'd like to double check the full code
export interface Student {
studentId: string
fullName: string
classSection: string
}
How To Use This Reference
Compare this reference with src/types/student.ts. Every property name and type should match exactly.
Build the form and table components
A controlled form keeps each input value in React state. The table receives the current collection through props and turns each student into a row.
- Select the src folder in the Explorer sidebar.
- Use the folder creation control at the top of the Explorer sidebar.
- Name the new folder components.
You should now see components beside types inside src.
- Select the components folder.
- Use the file creation control to create StudentForm.tsx.
- Build the controlled registration form by pasting this code into src/components/StudentForm.tsx:
import { useState } from 'react'
import type { Student } from '../types/student'
interface StudentFormProps { onAddStudent: (student: Student) => void }
const emptyForm: Student = { studentId: '', fullName: '', classSection: '' }
export function StudentForm({ onAddStudent }: StudentFormProps) {
const [form, setForm] = useState<Student>(emptyForm)
return <section className="panel"><h2>Register a student</h2><form className="student-form" onSubmit={(event) => { event.preventDefault(); onAddStudent(form); setForm(emptyForm) }}><label htmlFor="student-id">Student ID</label><input id="student-id" value={form.studentId} onChange={(event) => setForm({ ...form, studentId: event.target.value })} /><label htmlFor="full-name">Full name</label><input id="full-name" value={form.fullName} onChange={(event) => setForm({ ...form, fullName: event.target.value })} /><label htmlFor="class-section">Class section</label><input id="class-section" value={form.classSection} onChange={(event) => setForm({ ...form, classSection: event.target.value })} /><button type="submit">Register student</button></form></section>
}
How Does the Form Work?
- The form state holds the current values from all three inputs.
- Each onChange handler creates an updated form object with the edited field.
- The submit handler prevents the browser's default form submission.
- The onAddStudent prop sends the completed record to the parent component.
- The setForm call clears the inputs after submission.
- Save src/components/StudentForm.tsx.
- Confirm that the Explorer lists StudentForm.tsx inside src/components.
Form File Showing Errors?
Confirm that the import path is ../types/student. Check that every input reads from the matching property on form.
Ask for help with the controlled form.
✔️ Awesome, I've got everything!
Your form component now owns its three input values and can submit one typed student record.
ⓧ I'd like to double check the full code
import { useState } from 'react'
import type { Student } from '../types/student'
interface StudentFormProps { onAddStudent: (student: Student) => void }
const emptyForm: Student = { studentId: '', fullName: '', classSection: '' }
export function StudentForm({ onAddStudent }: StudentFormProps) {
const [form, setForm] = useState<Student>(emptyForm)
return <section className="panel"><h2>Register a student</h2><form className="student-form" onSubmit={(event) => { event.preventDefault(); onAddStudent(form); setForm(emptyForm) }}><label htmlFor="student-id">Student ID</label><input id="student-id" value={form.studentId} onChange={(event) => setForm({ ...form, studentId: event.target.value })} /><label htmlFor="full-name">Full name</label><input id="full-name" value={form.fullName} onChange={(event) => setForm({ ...form, fullName: event.target.value })} /><label htmlFor="class-section">Class section</label><input id="class-section" value={form.classSection} onChange={(event) => setForm({ ...form, classSection: event.target.value })} /><button type="submit">Register student</button></form></section>
}
How To Use This Reference
Compare the imports, props, state, form fields, and submit handler with your saved file.
- Select the components folder in the Explorer sidebar.
- Use the file creation control to create StudentTable.tsx.
- Build the registry table by pasting this code into src/components/StudentTable.tsx:
import type { Student } from '../types/student'
interface StudentTableProps { students: Student[] }
export function StudentTable({ students }: StudentTableProps) {
return <section className="panel"><h2>Registry</h2>{students.length === 0 ? <p>No students yet</p> : <table><thead><tr><th>Student ID</th><th>Full name</th><th>Class section</th></tr></thead><tbody>{students.map((student) => <tr key={student.studentId}><td>{student.studentId}</td><td>{student.fullName}</td><td>{student.classSection}</td></tr>)}</tbody></table>}</section>
}
How Does the Table Work?
- The students prop is a typed array of student records.
- The empty-state message appears when the array contains no records.
- The map() call creates one table row for each student.
- The studentId value gives React a stable key for each row.
- Save src/components/StudentTable.tsx.
- Confirm that the Explorer lists both component files inside src/components.
Table File Showing Errors?
Check that students uses the Student[] type. Confirm that the callback inside map() returns one complete table row.
Ask for help with the table component.
✔️ Awesome, I've got everything!
Your table component can now render any typed student array it receives.
ⓧ I'd like to double check the full code
import type { Student } from '../types/student'
interface StudentTableProps { students: Student[] }
export function StudentTable({ students }: StudentTableProps) {
return <section className="panel"><h2>Registry</h2>{students.length === 0 ? <p>No students yet</p> : <table><thead><tr><th>Student ID</th><th>Full name</th><th>Class section</th></tr></thead><tbody>{students.map((student) => <tr key={student.studentId}><td>{student.studentId}</td><td>{student.fullName}</td><td>{student.classSection}</td></tr>)}</tbody></table>}</section>
}
How To Use This Reference
Compare the prop definition, empty state, headers, and mapped row with your saved file.
Connect state and test refresh loss
The parent App component owns the full student collection. This gives the form one place to add records and the table one source to display.
- Return to index.html from earlier.
- Replace the file contents with this document shell:
<!doctype html>
<html lang="en">
<head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta name="description" content="A fictional student registry for the Campus Grade Generator demo" /><title>Campus Grade Generator</title></head>
<body><div id="root"></div><script type="module" src="/src/main.tsx"></script></body>
</html>
What Does the Document Shell Do?
- The description identifies the page as a fictional student registry demo.
- The title names the browser tab Campus Grade Generator.
- The root element remains the mounting point for the React application.
- Save index.html.
- Confirm that the browser tab still reads Campus Grade Generator.
✔️ Awesome, I've got everything!
Your document shell now describes the fictional registry and keeps the correct page title.
ⓧ I'd like to double check the full code
<!doctype html>
<html lang="en">
<head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta name="description" content="A fictional student registry for the Campus Grade Generator demo" /><title>Campus Grade Generator</title></head>
<body><div id="root"></div><script type="module" src="/src/main.tsx"></script></body>
</html>
How To Use This Reference
Compare the metadata, title, root element, and module script with your saved file.
- Return to src/App.tsx from earlier.
- Replace the starter component with this registry state and layout:
import { useState } from 'react'
import './App.css'
import { StudentForm } from './components/StudentForm'
import { StudentTable } from './components/StudentTable'
import type { Student } from './types/student'
function App() {
const [students, setStudents] = useState<Student[]>([])
function addStudent(student: Student) { setStudents([...students, student]) }
return <main className="app-shell"><header><h1>Student Registry</h1><p>Register fictional students now. Reuse their IDs for QR codes, assessments, grades, and reports in later stages.</p></header><div className="dashboard-grid"><StudentForm onAddStudent={addStudent} /><StudentTable students={students} /></div></main>
}
export default App
How Does App Manage the Registry?
- The students state starts as an empty typed array.
- The addStudent function builds a new array with the existing records plus the submitted student.
- The spread syntax keeps the state update immutable.
- The form receives the add function through onAddStudent.
- The table receives the current collection through students.
- Save src/App.tsx.
- Return to the local Vite page in your browser.
You should see the Student Registry heading, three form fields, and the No students yet message.
Registry Page Not Loading?
Check that the component imports use ./components/StudentForm and ./components/StudentTable. Confirm that both component files are saved.
Ask for help with the component connection.
✔️ Awesome, I've got everything!
Your form and table now share one student collection owned by App.
ⓧ I'd like to double check the full code
import { useState } from 'react'
import './App.css'
import { StudentForm } from './components/StudentForm'
import { StudentTable } from './components/StudentTable'
import type { Student } from './types/student'
function App() {
const [students, setStudents] = useState<Student[]>([])
function addStudent(student: Student) { setStudents([...students, student]) }
return <main className="app-shell"><header><h1>Student Registry</h1><p>Register fictional students now. Reuse their IDs for QR codes, assessments, grades, and reports in later stages.</p></header><div className="dashboard-grid"><StudentForm onAddStudent={addStudent} /><StudentTable students={students} /></div></main>
}
export default App
How To Use This Reference
Compare the imports, state declaration, add function, and component props with your saved file.
The registry is ready for its first fictional record. This test proves that a form submission can update the table immediately.
- Enter STU-001 in the Student ID field.
- Enter Amina Reyes in the Full name field.
- Enter Year 8 - Maple in the Class section field.
- Click Register student.
That first record works. You should see one row containing STU-001, Amina Reyes, and Year 8 - Maple.
Before you refresh, do you expect the student row to remain in the registry?
- Refresh the browser page.
You should see the table return to No students yet. The student existed only in React state, so the browser discarded it when the page restarted.
Why Did the Record Disappear?
React state lives in the current page session. Refreshing the page creates a new App instance with an empty students array.
This is the intended shortfall for the in-memory registry. The next step adds a localStorage boundary so records can survive that restart.
Your typed form and table now work as one in-memory registry. Next, you will preserve the same collection across browser refreshes.
Persist Students with Local Storage
Your React registry now adds fictional students to an in-memory array. A browser refresh clears that array because the records exist only for the current page session.
The browser's localStorage can retain strings for the local Vite origin. A storage boundary converts the typed student collection to JSON for saving.
You will connect that boundary to the app. Each successful change reaches browser storage before it reaches the screen.
In this step, get ready to:
- Create a typed storage helper that validates restored student records.
- Load saved students when the app starts.
- Save collection changes before updating the registry.
Create the storage boundary
Browser storage accepts strings. Your helper serializes the student array when saving. It also checks parsed values before returning them to the app.
- In the Visual Studio Code Explorer sidebar from earlier, use the folder control to create storage inside src.
- Inside src/storage, use the file control to create studentStorage.ts.
The Explorer now shows src/storage/studentStorage.ts. This keeps browser persistence separate from the interface and UI components.
- Add the storage key, record checker, and loading function to studentStorage.ts by pasting this code:
import type { Student } from '../types/student'
const STORAGE_KEY = 'campus-grade-generator.students.v1'
function isStudent(value: unknown): value is Student {
if (typeof value !== 'object' || value === null) return false
const candidate = value as Record<string, unknown>
return typeof candidate.studentId === 'string' && typeof candidate.fullName === 'string' && typeof candidate.classSection === 'string'
}
export function loadStudents(): Student[] {
try {
const savedValue = localStorage.getItem(STORAGE_KEY)
if (savedValue === null) return []
const parsedValue: unknown = JSON.parse(savedValue)
return Array.isArray(parsedValue) ? parsedValue.filter(isStudent) : []
} catch { return [] }
}
How Does Loading Stay Safe?
- The versioned key campus-grade-generator.students.v1 gives this data format a stable browser storage location.
- The isStudent() type guard accepts records whose three required fields contain strings.
- The JSON.parse() call converts the saved string into a value that TypeScript treats as unknown until validation.
- The catch block returns an empty array when stored data cannot be read safely.
- Save studentStorage.ts.
- In Visual Studio Code, start a second PowerShell terminal inside campus-grade-generator.
Before you check the helper, predict whether the new file satisfies the project's current type rules.
- Check the loading helper by running this command in the second PowerShell terminal:
npm run build
What Does This Check Prove?
The build checks that the storage key, type guard, and loading function are valid project code. You should return to the PowerShell prompt without a TypeScript error.
Your registry now has a safe read path for saved records. The next function gives collection changes a write path.
Build Check Not Completing?
Confirm that the second PowerShell terminal is inside campus-grade-generator. Check that studentStorage.ts sits inside src/storage.
Compare every quote and brace in loadStudents() with the code above.
Help me diagnose the storage helper build failure.
- Add the saving function beneath loadStudents() by pasting this code:
export function saveStudents(students: Student[]): boolean {
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(students)); return true } catch { return false }
}
How Does Saving Report Success?
- The JSON.stringify() call serializes the student array into the string format browser storage accepts.
- The localStorage.setItem() call writes that string under the versioned key.
- The boolean return value tells the app whether it is safe to show the requested collection change.
- Save studentStorage.ts.
- Return to the second PowerShell terminal from earlier.
- Check the complete storage helper by running:
npm run build
What Should the Build Confirm?
You should return to the PowerShell prompt without a TypeScript error. That result confirms both storage functions use the shared Student type correctly.
That is the storage boundary complete. It can now restore a valid array or report whether a write succeeded.
Seeing a Storage Helper Build Failure?
Confirm that saveStudents() appears after the closing brace of loadStudents().
Check that the function parameter uses Student[] and that its return type is boolean.
Help me fix the save function build failure.
Connect storage to the app
The app needs one source of truth for each collection change. It calculates nextStudents, saves that array, and updates React state only after the write succeeds.
- Return to src/App.tsx in Visual Studio Code.
- Replace the current contents of App.tsx with this code:
import { useState } from 'react'
import './App.css'
import { StudentForm } from './components/StudentForm'
import { StudentTable } from './components/StudentTable'
import { loadStudents, saveStudents } from './storage/studentStorage'
import type { Student } from './types/student'
function App() {
const [students, setStudents] = useState<Student[]>(loadStudents)
const [storageError, setStorageError] = useState('')
function addStudent(student: Student): boolean { const nextStudents = [...students, student]; if (!saveStudents(nextStudents)) { setStorageError('Storage is unavailable. The new record was not added.'); return false }; setStudents(nextStudents); setStorageError(''); return true }
function deleteStudent(studentId: string) { const nextStudents = students.filter((student) => student.studentId !== studentId); if (!saveStudents(nextStudents)) { setStorageError('Storage is unavailable. The record was not deleted.'); return }; setStudents(nextStudents); setStorageError('') }
return <main className="app-shell"><header><h1>Student Registry</h1></header>{storageError !== '' && <p role="alert">{storageError}</p>}<div className="dashboard-grid"><StudentForm students={students} onAddStudent={addStudent} /><StudentTable students={students} onDeleteStudent={deleteStudent} /></div></main>
}
export default App
How Does the App Stay Synchronized?
- Passing loadStudents into useState() restores validated records when the app starts.
- The addStudent() function creates a new array before saving it.
- The deleteStudent() function prepares deletion with filter() before saving the remaining records.
- The storageError state gives failed writes a visible message without displaying an unsaved change.
- Save App.tsx.
- Return to the browser tab from earlier.
The Vite page reloads with the Student Registry heading. The registration form and registry remain available for the persistence test.
Browser Page Not Rendering?
Confirm that studentStorage.ts is inside src/storage. Check that the import path in App.tsx matches that location.
Keep the original PowerShell development server running. Return to its local Vite URL after correcting the file path.
Help me restore the app after connecting local storage.
✔️ Awesome, I've got everything!
Great. Save src/storage/studentStorage.ts and src/App.tsx before testing the refresh cycle.
ⓧ I'd like to double check the full code
Compare your storage helper with this complete file.
import type { Student } from '../types/student'
const STORAGE_KEY = 'campus-grade-generator.students.v1'
function isStudent(value: unknown): value is Student {
if (typeof value !== 'object' || value === null) return false
const candidate = value as Record<string, unknown>
return typeof candidate.studentId === 'string' && typeof candidate.fullName === 'string' && typeof candidate.classSection === 'string'
}
export function loadStudents(): Student[] {
try {
const savedValue = localStorage.getItem(STORAGE_KEY)
if (savedValue === null) return []
const parsedValue: unknown = JSON.parse(savedValue)
return Array.isArray(parsedValue) ? parsedValue.filter(isStudent) : []
} catch { return [] }
}
export function saveStudents(students: Student[]): boolean {
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(students)); return true } catch { return false }
}
Compare your app with this complete file.
import { useState } from 'react'
import './App.css'
import { StudentForm } from './components/StudentForm'
import { StudentTable } from './components/StudentTable'
import { loadStudents, saveStudents } from './storage/studentStorage'
import type { Student } from './types/student'
function App() {
const [students, setStudents] = useState<Student[]>(loadStudents)
const [storageError, setStorageError] = useState('')
function addStudent(student: Student): boolean { const nextStudents = [...students, student]; if (!saveStudents(nextStudents)) { setStorageError('Storage is unavailable. The new record was not added.'); return false }; setStudents(nextStudents); setStorageError(''); return true }
function deleteStudent(studentId: string) { const nextStudents = students.filter((student) => student.studentId !== studentId); if (!saveStudents(nextStudents)) { setStorageError('Storage is unavailable. The record was not deleted.'); return }; setStudents(nextStudents); setStorageError('') }
return <main className="app-shell"><header><h1>Student Registry</h1></header>{storageError !== '' && <p role="alert">{storageError}</p>}<div className="dashboard-grid"><StudentForm students={students} onAddStudent={addStudent} /><StudentTable students={students} onDeleteStudent={deleteStudent} /></div></main>
}
export default App
Test the full storage cycle
The decisive check is a browser refresh. A restored row proves that the app saved the collection under the local Vite origin and loaded it during the next startup.
- Enter STU-002 in the Student ID field.
- Enter Noah Chen in the Full name field.
- Enter Year 9 - Cedar in the Class section field.
- Click Register student.
You should see one registry row for STU-002, Noah Chen, and Year 9 - Cedar.
Before you refresh, do you expect the new row to survive the page reload?
- Refresh the browser page.
The same Noah Chen row returns after the refresh. That restored record proves the registry now reads and writes the same typed collection through browser storage.
Student Row Disappeared After Refresh?
Confirm that useState<Student[]>(loadStudents) passes the function itself as the initializer. Check that addStudent() calls saveStudents(nextStudents) before setStudents(nextStudents).
Keep the test on the same local Vite origin printed by the development server. Browser storage belongs to that origin.
Help me diagnose why the student row disappears after refresh.
You fixed the registry's refresh data loss. Next, you will add input guardrails so teachers can search records and remove saved entries safely.
Add Validation, Search, and Deletion
Your React registry already restores saved students after a refresh. You can now turn those persistent records into a registry that stays useful as the list grows.
Persistent data needs guardrails because blank fields or repeated IDs can make records unreliable. Search and deletion also need to keep the screen synchronized with localStorage.
In this step, get ready to:
- Protect the form with required-field checks and duplicate-ID prevention.
- Search the registry across IDs, names, and class sections.
- Delete saved students while keeping browser storage synchronized.
Finish form validation
A controlled form can normalize its values before accepting a student. The form can also use the existing student collection to detect a repeated ID before saving anything.
- In the Explorer sidebar in Visual Studio Code, select src/components/StudentForm.tsx.
- Replace the contents of src/components/StudentForm.tsx by pasting the code below:
import { useState } from 'react'
import type { Student } from '../types/student'
interface StudentFormProps { students: Student[]; onAddStudent: (student: Student) => boolean }
const emptyForm: Student = { studentId: '', fullName: '', classSection: '' }
export function StudentForm({ students, onAddStudent }: StudentFormProps) {
const [form, setForm] = useState<Student>(emptyForm)
const [message, setMessage] = useState('')
const [isError, setIsError] = useState(false)
function submitStudent(event: React.FormEvent<HTMLFormElement>) { event.preventDefault(); const nextStudent: Student = { studentId: form.studentId.trim().toUpperCase(), fullName: form.fullName.trim(), classSection: form.classSection.trim() }; if (nextStudent.studentId === '' || nextStudent.fullName === '' || nextStudent.classSection === '') { setMessage('Complete every field before registering the student.'); setIsError(true); return }; if (students.some((student) => student.studentId.toLowerCase() === nextStudent.studentId.toLowerCase())) { setMessage('That student ID is already registered.'); setIsError(true); return }; if (!onAddStudent(nextStudent)) { setMessage('The browser could not save this student. Try again.'); setIsError(true); return }; setForm(emptyForm); setMessage(`${nextStudent.fullName} was registered.`); setIsError(false) }
return <section className="panel"><h2>Register a student</h2><form className="student-form" onSubmit={submitStudent}><input value={form.studentId} onChange={(event) => setForm({ ...form, studentId: event.target.value })} placeholder="STU-001" /><input value={form.fullName} onChange={(event) => setForm({ ...form, fullName: event.target.value })} placeholder="Amina Reyes" /><input value={form.classSection} onChange={(event) => setForm({ ...form, classSection: event.target.value })} placeholder="Year 8 - Maple" /><button type="submit">Register student</button>{message !== '' && <p role="status">{message}</p>}</form></section>
}
What does this code do?
- The emptyForm object gives the component one reusable value for its initial state and successful reset.
- The submitStudent() function trims every field and converts the student ID to uppercase before validation.
- The required-field check stops an incomplete record before it reaches onAddStudent.
- The duplicate check compares IDs without case sensitivity, so different letter casing cannot create repeated records.
- The form clears only when onAddStudent returns true, which confirms that the new collection was saved.
- Save src/components/StudentForm.tsx.
- Switch back to the browser from earlier.
- Click Register student while all three inputs are empty.
You should see Complete every field before registering the student. below the button.
- Enter STU-002 in the first input.
- Enter Noah Chen in the second input.
- Enter Year 9 - Cedar in the third input.
- Click Register student.
You should see That student ID is already registered. The saved Noah Chen row remains unchanged.
Form checks not appearing?
Confirm that the form uses onSubmit={submitStudent}. Check that each early validation branch ends with return.
If the form still accepts an invalid record, help me compare my StudentForm validation with the expected component.
Add registry search and deletion
A registry becomes harder to use when every row is always visible. A normalized search term lets one filter match student IDs, names, and class sections without treating uppercase letters differently.
- Return to the Explorer sidebar.
- Select src/components/StudentTable.tsx.
- Replace the contents of src/components/StudentTable.tsx by pasting the code below:
import { useState } from 'react'
import type { Student } from '../types/student'
interface StudentTableProps { students: Student[]; onDeleteStudent: (studentId: string) => void }
export function StudentTable({ students, onDeleteStudent }: StudentTableProps) {
const [searchTerm, setSearchTerm] = useState('')
const normalizedSearch = searchTerm.trim().toLowerCase()
const filteredStudents = students.filter((student) => [student.studentId, student.fullName, student.classSection].some((value) => value.toLowerCase().includes(normalizedSearch)))
return <section className="panel registry-panel"><h2>Registry</h2><span>{filteredStudents.length} shown</span><label htmlFor="student-search">Search students</label><input id="student-search" value={searchTerm} onChange={(event) => setSearchTerm(event.target.value)} />{filteredStudents.length === 0 ? <p>No matching students</p> : <table><tbody>{filteredStudents.map((student) => <tr key={student.studentId}><td>{student.studentId}</td><td>{student.fullName}</td><td>{student.classSection}</td><td><button type="button" onClick={() => onDeleteStudent(student.studentId)}>Delete</button></td></tr>)}</tbody></table>}</section>
}
What does this code do?
- The searchTerm state tracks what you type into the search input.
- The normalizedSearch value removes outside spaces and converts the query to lowercase.
- The filteredStudents collection keeps a student when any searchable field contains the normalized query.
- The result count and empty state both use the filtered collection, so they update with every search.
- Each Delete button passes its row's ID to onDeleteStudent, which already saves the smaller collection before updating the screen.
- Save src/components/StudentTable.tsx.
- Switch back to the browser from earlier.
- Enter Noah in the Search students input.
You should see 1 shown with the Noah Chen row beneath it.
- Replace the search text with ZZZ.
You should see 0 shown with the No matching students empty state.
- Clear the Search students input.
Search results not changing?
Check that the input's value uses searchTerm. Confirm that its change handler calls setSearchTerm.
If every row still appears, help me debug my StudentTable search filter.
✔️ Awesome, I've got everything!
Both components now match the complete Stage 1 registry. The existing application and storage files remain unchanged.
ⓧ I'd like to double check the full code
These files are the exact end state for this step.
import { useState } from 'react'
import './App.css'
import { StudentForm } from './components/StudentForm'
import { StudentTable } from './components/StudentTable'
import { loadStudents, saveStudents } from './storage/studentStorage'
import type { Student } from './types/student'
function App() {
const [students, setStudents] = useState<Student[]>(loadStudents)
const [storageError, setStorageError] = useState('')
function addStudent(student: Student): boolean { const nextStudents = [...students, student]; if (!saveStudents(nextStudents)) { setStorageError('Storage is unavailable. The new record was not added.'); return false }; setStudents(nextStudents); setStorageError(''); return true }
function deleteStudent(studentId: string) { const nextStudents = students.filter((student) => student.studentId !== studentId); if (!saveStudents(nextStudents)) { setStorageError('Storage is unavailable. The record was not deleted.'); return }; setStudents(nextStudents); setStorageError('') }
return <main className="app-shell"><header><h1>Student Registry</h1></header>{storageError !== '' && <p role="alert">{storageError}</p>}<div className="dashboard-grid"><StudentForm students={students} onAddStudent={addStudent} /><StudentTable students={students} onDeleteStudent={deleteStudent} /></div></main>
}
export default App
import { useState } from 'react'
import type { Student } from '../types/student'
interface StudentFormProps { students: Student[]; onAddStudent: (student: Student) => boolean }
const emptyForm: Student = { studentId: '', fullName: '', classSection: '' }
export function StudentForm({ students, onAddStudent }: StudentFormProps) {
const [form, setForm] = useState<Student>(emptyForm)
const [message, setMessage] = useState('')
const [isError, setIsError] = useState(false)
function submitStudent(event: React.FormEvent<HTMLFormElement>) { event.preventDefault(); const nextStudent: Student = { studentId: form.studentId.trim().toUpperCase(), fullName: form.fullName.trim(), classSection: form.classSection.trim() }; if (nextStudent.studentId === '' || nextStudent.fullName === '' || nextStudent.classSection === '') { setMessage('Complete every field before registering the student.'); setIsError(true); return }; if (students.some((student) => student.studentId.toLowerCase() === nextStudent.studentId.toLowerCase())) { setMessage('That student ID is already registered.'); setIsError(true); return }; if (!onAddStudent(nextStudent)) { setMessage('The browser could not save this student. Try again.'); setIsError(true); return }; setForm(emptyForm); setMessage(`${nextStudent.fullName} was registered.`); setIsError(false) }
return <section className="panel"><h2>Register a student</h2><form className="student-form" onSubmit={submitStudent}><input value={form.studentId} onChange={(event) => setForm({ ...form, studentId: event.target.value })} placeholder="STU-001" /><input value={form.fullName} onChange={(event) => setForm({ ...form, fullName: event.target.value })} placeholder="Amina Reyes" /><input value={form.classSection} onChange={(event) => setForm({ ...form, classSection: event.target.value })} placeholder="Year 8 - Maple" /><button type="submit">Register student</button>{message !== '' && <p role="status">{message}</p>}</form></section>
}
import { useState } from 'react'
import type { Student } from '../types/student'
interface StudentTableProps { students: Student[]; onDeleteStudent: (studentId: string) => void }
export function StudentTable({ students, onDeleteStudent }: StudentTableProps) {
const [searchTerm, setSearchTerm] = useState('')
const normalizedSearch = searchTerm.trim().toLowerCase()
const filteredStudents = students.filter((student) => [student.studentId, student.fullName, student.classSection].some((value) => value.toLowerCase().includes(normalizedSearch)))
return <section className="panel registry-panel"><h2>Registry</h2><span>{filteredStudents.length} shown</span><label htmlFor="student-search">Search students</label><input id="student-search" value={searchTerm} onChange={(event) => setSearchTerm(event.target.value)} />{filteredStudents.length === 0 ? <p>No matching students</p> : <table><tbody>{filteredStudents.map((student) => <tr key={student.studentId}><td>{student.studentId}</td><td>{student.fullName}</td><td>{student.classSection}</td><td><button type="button" onClick={() => onDeleteStudent(student.studentId)}>Delete</button></td></tr>)}</tbody></table>}</section>
}
import type { Student } from '../types/student'
const STORAGE_KEY = 'campus-grade-generator.students.v1'
function isStudent(value: unknown): value is Student {
if (typeof value !== 'object' || value === null) return false
const candidate = value as Record<string, unknown>
return typeof candidate.studentId === 'string' && typeof candidate.fullName === 'string' && typeof candidate.classSection === 'string'
}
export function loadStudents(): Student[] {
try { const savedValue = localStorage.getItem(STORAGE_KEY); if (savedValue === null) return []; const parsedValue: unknown = JSON.parse(savedValue); return Array.isArray(parsedValue) ? parsedValue.filter(isStudent) : [] } catch { return [] }
}
export function saveStudents(students: Student[]): boolean {
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(students)); return true } catch { return false }
}
export interface Student {
studentId: string
fullName: string
classSection: string
}
Run the complete registry test
The final test uses three fictional students to exercise every path you added. Each successful registration should update the table immediately.
- Enter STU-001 in the first form input.
- Enter Amina Reyes in the second form input.
- Enter Year 8 - Maple in the third form input.
- Click Register student.
You should see the Amina Reyes row appear with Amina Reyes was registered. below the form.
- Enter STU-003 in the first form input.
- Enter Lila Santos in the second form input.
- Enter Year 8 - Maple in the third form input.
- Click Register student.
Registry checkpoint
Your registry should now contain Amina Reyes, Noah Chen, and Lila Santos. The result count should show 3 shown.
Before you submit the next record, do you think changing the letter casing of an existing ID can bypass the duplicate check?
- Enter stu-002 in the first form input.
- Enter Noah Chen in the second form input.
- Enter Year 9 - Cedar in the third form input.
- Click Register student.
You should see That student ID is already registered. The count stays at 3 shown because the case-insensitive check blocked the duplicate.
Deletion uses the same saved collection as registration. Removing a filtered row should update both the visible results and browser storage.
- Enter Lila in the Search students input.
- Click Delete on the STU-003 row.
You should see 0 shown because the only row matching Lila has been removed.
- Clear the Search students input.
- Enter Amina in the Search students input.
- Click Delete on the STU-001 row.
You should see 0 shown because Amina Reyes is no longer in the saved collection.
- Clear the Search students input.
Before you refresh, which student do you expect the registry to restore from storage?
- Refresh the browser.
You should see only STU-002, Noah Chen, and Year 9 - Cedar. You have completed the Stage 1 registry with validation, search, deletion, and synchronized browser storage.
Deleted students returning after refresh?
Confirm that deleteStudent() passes nextStudents to saveStudents() before calling setStudents().
If the removed row still returns, help me trace my deletion flow from StudentTable to localStorage.
Secret mission
Filter by Class Section
A growing registry needs a faster way to focus on one class. Add a class-section filter that works with text search while keeping the student model and saved records unchanged.
Clean Up Your Resources
Clean Up Your Resources
Everything in this project runs locally on your Windows computer. Keeping these resources costs $0.
Choose whether to keep the registry running, pause your work for Stage 2, or delete the project entirely.
Resources you used:
- The campus-grade-generator project folder with its installed npm dependencies.
- The Vite development server running in PowerShell.
- Fictional student records saved through localStorage for the local Vite origin.
Keep everything running
No action is needed. Choose this if you plan to continue with the Stage 2 QR work soon.
- Keep PowerShell open so the development server remains available.
- Leave the campus-grade-generator folder in its current location.
- Leave the fictional student records in browser storage for the next stage.
Pause - I'll come back to this later
Stop the development server to free the PowerShell session. Your project files remain ready for your next session.
- Switch back to PowerShell.
- Use the PowerShell interrupt shortcut to stop the Vite development server.
- Confirm that the PowerShell prompt accepts input again.
- Leave the campus-grade-generator folder in place.
- Keep the fictional student records in browser storage.
Your server is now paused. The project files and saved registry records remain available for Stage 2.
Delete - I don't want to use this again
Remove every local project resource when you no longer need Stage 1. Follow the sequence below so the browser records are cleared before the local app disappears.
- Switch back to the student registry in your browser.
- Clear the Search students field.
- Select All sections from the Class section control.
- Click Delete beside each fictional student.
You should see No matching students. The registry count drops to zero because the saved collection is now empty.
- Return to PowerShell.
- Use the PowerShell interrupt shortcut to stop the Vite development server.
The PowerShell prompt accepts input again. This confirms that the local development server has stopped.
- Return to Visual Studio Code.
- Right-click the campus-grade-generator folder in the Explorer sidebar.
- Choose the option that reveals the folder in the Windows file browser.
- Close Visual Studio Code.
- Select the campus-grade-generator folder in the Windows file browser.
- Delete the selected folder from your computer.
The campus-grade-generator folder should no longer appear in the file browser. Its installed npm dependencies are removed with it.
Nice Work!
Nice Work!
You did it! Your Stage 1 student registry is ready for fictional classroom demos.
You've learned how to:
- Scaffold a browser app with Vite. Structure a typed student registry with React components backed by a reusable TypeScript interface. Separate the student model from the interface. Keep browser storage in its own module.
- Register fictional students through a controlled form. Update the visible table with immutable React state. Protect the registry with required-field checks. Block duplicate student IDs.
- Create browser persistence by serializing student records with localStorage. Restore saved records after refreshes. Search across student IDs, names, or class sections. Delete records from the screen. Keep storage synchronized with every deletion.
- Secret Mission: Derive sorted class-section options from the current students. Combine the section filter with text search. Update the shown count without changing the student model or saved records.
Ready to quiz yourself?