Build a React Study Planner

Build a React planner that adds topics, tracks progress, and filters tasks.

Introduction

30 Second Summary

Study goals are easy to collect. Keeping track of what you have finished becomes harder as the list grows.

In this project, you will build a single-page study planner with React. The finished app responds instantly whenever your study plan changes.

What You'll Build

Picture ticking off the last topic in your browser as the progress display fills before a completion message celebrates your finished plan.

By the end of this project, you'll have:

  • A responsive study planner that remains clear on wide screens or narrow screens.
  • A controlled form that turns typed text into a new study topic without reloading the page.
  • Live completion tracking that updates each topic status while recalculating your progress immediately.
  • Secret Mission: Add a three-view filter for All, Active, or Done while keeping one topic list as the source of truth.

Are there any prerequisites?

Basic HTML knowledge plus CSS knowledge are enough.

No previous React experience is required.

Before We Start

Before the hands-on work begins, lock in the planner you are building and the problem it solves. You are committing to a React study planner that tracks learning topics and their completion status.

Set Up React and Vite

Your study planner now needs a local workspace where each React change can appear in the browser.

A fast local feedback loop keeps this project focused on React fundamentals. Vite provides that loop. Node.js runs Vite. npm installs the packages the project needs.

In this step, get ready to:
  • Verify the Node.js and npm toolchain.
  • Create a React project with Vite.
  • Install the pinned dependencies and run the starter page.
Verify Node.js and npm

Vite needs a compatible Node.js runtime before it can create or run your project. This project uses Node.js version 22.12.0 or newer as its baseline.

  • Open a PowerShell terminal in Visual Studio Code.
  • Check the installed Node.js version and npm version by running these commands:
node --version
npm --version

How Do These Checks Work?

  • The first command prints the installed Node.js version.
  • The second command confirms that npm is available.

✔️ I see the required version

Your Node.js output is version 22.12.0 or newer. npm also prints a version number.

Your JavaScript toolchain is ready for Vite.

ⓧ I see an older version

Your current Node.js version is below the project baseline. Upgrade to the current Node.js LTS release before creating the project.

  • Visit the official Node.js download page.
  • Download the Windows installer for Node.js version 24.21.0 LTS.
  • Complete the installer with its default options.
  • Close the existing PowerShell terminal.
  • Open a new PowerShell terminal in Visual Studio Code.
  • Check both versions again by running:
node --version
npm --version

What Confirms the Upgrade?

Node.js should now report version 22.12.0 or newer. npm should print its own version number.

🙋‍♀️ Still Seeing the Older Version?

Make sure you reopened the PowerShell terminal after completing the installer. A fresh terminal session picks up the updated Node.js installation.

Ask for help if the old version remains active: help me update the Node.js version used by PowerShell in Visual Studio Code.

ⓧ A command is not found

Node.js or npm is unavailable in the current terminal. Install the current Node.js LTS release to add both tools.

  • Visit the official Node.js download page.
  • Download the Windows installer for Node.js version 24.21.0 LTS.
  • Complete the installer with its default options.
  • Close the existing PowerShell terminal.
  • Open a new PowerShell terminal in Visual Studio Code.
  • Check both tools by running:
node --version
npm --version

What Confirms the Installation?

Node.js should print version 22.12.0 or newer. npm should print a version number on the next line.

🙋‍♀️ Commands Still Missing?

Confirm the Node.js installer finished successfully. Reopen Visual Studio Code before creating another PowerShell terminal.

Ask for help if PowerShell still cannot find the tools: help me make Node.js and npm available in the Visual Studio Code PowerShell terminal.

Scaffold and pin the project

Vite creates the folder structure that React needs. Its setup wizard also lets you choose the framework and language before any project files are generated.

Why Vite for This Project?

Vite keeps the project focused on components and state. A framework such as Next.js would introduce routing and server concepts before you need them.

  • Move to your Desktop by running this command:
cd ~/Desktop

What Does This Command Do?

This changes PowerShell's current location to your Desktop. Vite creates the project folder there so you can find it easily.

The scaffolding command starts an interactive wizard. Prepare these choices before you launch it.

  • Enter react-study-planner when Project name: appears.
  • Select React when Select a framework: appears.
  • Select JavaScript when Select a variant: appears.
  • Choose Oxlint when Which linter to use? appears.
  • Answer No when Install with npm and start now? appears.
  • Start the Vite setup wizard by running this command:
npm create vite@latest

What Does the Scaffold Create?

Vite creates the react-study-planner folder with the React entry point and development configuration.

  • The browser entry file is index.html.
  • React starts from src/main.jsx.
  • The first component lives in src/App.jsx.
  • Vite reads its configuration from vite.config.js.
  • Use Visual Studio Code's folder picker to open react-study-planner from your Desktop.

You should see package.json, index.html, vite.config.js, and the src folder in the Explorer sidebar.

The generated dependency list includes packages that this beginner project does not use. A minimal package.json keeps every runtime and build dependency pinned to an exact version.

  • Select package.json in the Explorer sidebar.
  • Replace its entire contents with the code below:
{"name":"react-study-planner","private":true,"version":"0.0.0","type":"module","scripts":{"dev":"vite","build":"vite build","preview":"vite preview"},"dependencies":{"react":"19.2.8","react-dom":"19.2.8"},"devDependencies":{"@vitejs/plugin-react":"6.1.1","vite":"8.2.2"}}

What Does This File Control?

  • The scripts define how npm starts and builds the project.
  • The dependencies pin React and React DOM to version 19.2.8.
  • The development dependencies pin Vite to version 8.2.2.
  • The React plugin is pinned to version 6.1.1.
  • Save package.json.
  • Install the pinned dependencies by running:
npm install

What Does npm Install?

npm reads package.json and installs its dependencies into the local node_modules folder. The exact version pins make the setup repeatable.

PowerShell returns to its prompt when installation finishes. You should also see a node_modules folder in the Explorer sidebar.

Dependency Installation Failed?

Confirm that PowerShell is running inside the react-study-planner folder. Check that package.json contains valid JSON with matching quotation marks.

Ask for help with the installation output: help me troubleshoot npm install in my React Vite project.

The dependency layer is now locked down. Your project has the exact React and Vite versions used throughout this build.

Prepare and run the starter page

The generated project includes a demonstration screen. You will first prove that Vite can serve it before reducing the files to a clean study-planner baseline.

  • Select index.html in the Explorer sidebar.
  • Replace its entire 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>React Study Planner</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.jsx"></script>
  </body>
</html>

What Does This Page Provide?

  • The root element gives React a place to render the app.
  • The module script loads src/main.jsx.
  • The page title identifies the project in the browser tab.
  • Save index.html.

Before you start the server, what do you expect Vite to show when the project loads successfully?

  • Start the local development server by running:
npm run dev

What Does the Development Server Do?

Vite serves the project locally and keeps this PowerShell terminal busy while the server is running. Leave the process running while you work on the app.

  • Open http://localhost:5173 in your browser.

You should see the React and Vite starter page. This proves the scaffold and pinned dependencies work together.

Starter Page Did Not Load?

Confirm that the PowerShell terminal is still running the development server. Check the terminal for a local URL before refreshing the browser.

Ask for help if the local page remains unavailable: help me troubleshoot why my Vite development server is not loading in the browser.

Your first local React page is live. The browser can now show every saved change through the running Vite server.

A smaller component gives the next step a clean starting point. It also makes the project name visible while you build the full planner.

  • Select src/App.jsx in the Explorer sidebar.
  • Replace its entire contents with the code below:
export default function App() {
  return <h1>React Study Planner</h1>
}

What Does This Component Do?

The App() component returns the heading that React renders into the page. This becomes the starting component for the static planner in the next step.

  • Save src/App.jsx.

Your browser should update to show the React Study Planner heading.

Heading Did Not Update?

Confirm that src/App.jsx is saved. Check that the development server is still running in PowerShell.

Ask for help with the component: help me find why my App component is not showing the React Study Planner heading.

The generated stylesheet contains presentation rules for the original demonstration screen. A minimal baseline removes that styling before you design the planner.

  • Select src/index.css in the Explorer sidebar.
  • Replace its entire contents with the code below:
body {
  margin: 0;
  font-family: system-ui, sans-serif;
}

What Does This Baseline Style Do?

  • The body margin is reset to 0.
  • The system font keeps the starter page readable without an external font dependency.

Before you save, how should the heading look after the original demonstration styles are removed?

  • Save src/index.css.
  • Refresh http://localhost:5173 in your browser.

You should see a plain React Study Planner heading using your system font. The page should have no outer body margin.

Still Seeing the Original Styles?

Confirm that src/main.jsx still imports ./index.css. Check that src/App.jsx no longer imports the generated App.css file.

Ask for help with the stylesheet connection: help me find why the old Vite styles are still affecting my React page.

✔️ Awesome, I've got everything!

Great work. Keep the Vite development server running while you continue to the static planner.

ⓧ I'd like to double check the full code

Compare each project file with the matching reference below.

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>React Study Planner</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.jsx"></script>
  </body>
</html>

What to Compare in index.html

Confirm the page title and the root element match the reference. The module script should load /src/main.jsx.

{"name":"react-study-planner","private":true,"version":"0.0.0","type":"module","scripts":{"dev":"vite","build":"vite build","preview":"vite preview"},"dependencies":{"react":"19.2.8","react-dom":"19.2.8"},"devDependencies":{"@vitejs/plugin-react":"6.1.1","vite":"8.2.2"}}

What to Compare in package.json

Confirm every dependency uses the pinned version shown here. The three npm scripts should also match exactly.

export default function App() {
  return <h1>React Study Planner</h1>
}

What to Compare in App.jsx

Confirm the component exports App() and returns the React Study Planner heading.

body {
  margin: 0;
  font-family: system-ui, sans-serif;
}

What to Compare in index.css

Confirm the body has no margin. The font stack should use system-ui before the generic sans-serif fallback.

import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <App />
  </StrictMode>,
)

What to Compare in main.jsx

Confirm the entry point imports the stylesheet and App. It should render the component inside StrictMode.

import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'

export default defineConfig({
  plugins: [react()],
})

What to Compare in vite.config.js

Confirm the configuration imports the React plugin. The plugin should appear inside the plugins array.

Your React workspace is installed and running through Vite. Next, you will turn this blank starting point into a responsive study planner.

Build the Static Planner

Your Vite starter page proves that the local development loop works. The next change replaces that starter screen with the structure of your study planner.

A polished layout can still be a static picture. This step makes the missing interactivity visible before you build the working React feature.

In this step, get ready to:
  • Build reusable static components around three sample topics.
  • Style the planner as a responsive single-page interface.
  • Submit the form to expose its page reload limitation.
Build the static component tree

Each component owns one visible part of the planner. JSX lets those parts describe their browser structure.

The StudyItem component receives each topic through props. This keeps the row reusable across all three sample topics.

  • Select src/App.jsx in the Explorer sidebar.
  • Select every line in src/App.jsx by pressing Ctrl+A.
  • Replace the selected code by pasting the component tree below:
const topics = [
  { id: 1, title: 'Review JavaScript arrays', done: true },
  { id: 2, title: 'Practice React components', done: false },
  { id: 3, title: 'Style a responsive layout', done: false },
]

function StudyForm() {
  return (
    <form className="study-form">
      <label htmlFor="topic">New study topic</label>
      <div className="form-row">
        <input id="topic" type="text" placeholder="Example: Practice JavaScript functions" />
        <button type="submit">Add topic</button>
      </div>
    </form>
  )
}

function Progress() {
  return <section className="progress-card" aria-label="Study progress"><div><span className="progress-label">Progress</span><strong>1 of 3 complete</strong></div><progress value="1" max="3" /></section>
}

function StudyItem({ topic }) {
  return <label className={`study-item ${topic.done ? 'complete' : ''}`}><input type="checkbox" defaultChecked={topic.done} /><span className="topic-title">{topic.title}</span><span className="status">{topic.done ? 'Done' : 'Active'}</span></label>
}

export default function App() {
  return <main className="app-shell"><header className="hero"><p className="eyebrow">React practice project</p><h1>Study Planner</h1><p>Turn your learning goals into a focused checklist.</p></header><StudyForm /><Progress /><section className="topic-section" aria-labelledby="topic-heading"><div className="section-heading"><h2 id="topic-heading">Today's topics</h2><span>3 total</span></div><div className="topic-list">{topics.map((topic) => <StudyItem key={topic.id} topic={topic} />)}</div></section></main>
}

What does this code do?

  • The topics array holds the three sample study topics.
  • The StudyForm component renders the topic field plus its submit button.
  • The Progress component displays a fixed count for the static planner.
  • The StudyItem component changes each row using its topic data.
  • The App component uses map() to render one reusable row for every topic.
  • Save src/App.jsx by pressing Ctrl+S.
  • Return to the browser tab running the planner.

You'll see an unstyled planner containing the form. The page also shows 1 of 3 complete above three topic rows.

Good progress. Your browser is now rendering all four static components.

Seeing a blank page?

Compare your src/App.jsx with the full-file tab below. Pay close attention to the closing brackets around topics.map().

Ask for help with the exact file if you still cannot see the planner: help me find the JSX syntax problem in my static React study planner

✔️ Awesome, I've got everything!

Your four static components are saved in src/App.jsx.

ⓧ I'd like to double check the full code

const topics = [
  { id: 1, title: 'Review JavaScript arrays', done: true },
  { id: 2, title: 'Practice React components', done: false },
  { id: 3, title: 'Style a responsive layout', done: false },
]

function StudyForm() {
  return (
    <form className="study-form">
      <label htmlFor="topic">New study topic</label>
      <div className="form-row">
        <input id="topic" type="text" placeholder="Example: Practice JavaScript functions" />
        <button type="submit">Add topic</button>
      </div>
    </form>
  )
}

function Progress() {
  return <section className="progress-card" aria-label="Study progress"><div><span className="progress-label">Progress</span><strong>1 of 3 complete</strong></div><progress value="1" max="3" /></section>
}

function StudyItem({ topic }) {
  return <label className={`study-item ${topic.done ? 'complete' : ''}`}><input type="checkbox" defaultChecked={topic.done} /><span className="topic-title">{topic.title}</span><span className="status">{topic.done ? 'Done' : 'Active'}</span></label>
}

export default function App() {
  return <main className="app-shell"><header className="hero"><p className="eyebrow">React practice project</p><h1>Study Planner</h1><p>Turn your learning goals into a focused checklist.</p></header><StudyForm /><Progress /><section className="topic-section" aria-labelledby="topic-heading"><div className="section-heading"><h2 id="topic-heading">Today's topics</h2><span>3 total</span></div><div className="topic-list">{topics.map((topic) => <StudyItem key={topic.id} topic={topic} />)}</div></section></main>
}
Style the responsive planner

The component tree gives the browser its content. CSS now turns that structure into a centered planner with distinct cards plus readable topic rows.

A media query stacks the form plus progress layout on narrow screens. The same interface stays usable on desktop and mobile widths.

  • Select src/index.css in the Explorer sidebar.
  • Select every line in src/index.css by pressing Ctrl+A.
  • Replace the selected code by pasting the planner styles below:
@import url('data:text/css,');
:root{font-family:Inter,ui-sans-serif,system-ui,sans-serif;color:#172033;background:#eef2ff}*{box-sizing:border-box}body{margin:0;min-width:320px;min-height:100vh;background:radial-gradient(circle at top left,#c7d2fe 0,transparent 35%),#eef2ff}button,input{font:inherit}button{cursor:pointer}#root{min-height:100vh}.app-shell{width:min(100% - 32px,760px);margin:0 auto;padding:48px 0}.hero{margin-bottom:28px}.eyebrow{margin:0 0 8px;color:#4f46e5;font-size:.78rem;font-weight:800;letter-spacing:.12em;text-transform:uppercase}h1,h2,p{margin-top:0}h1{margin-bottom:8px;font-size:clamp(2.2rem,8vw,4rem);line-height:1}.hero>p:last-child{max-width:540px;color:#566079;font-size:1.05rem}.study-form,.progress-card,.topic-section{border:1px solid #d8def0;border-radius:18px;background:rgba(255,255,255,.92);box-shadow:0 16px 45px rgba(74,77,120,.1)}.study-form{margin-bottom:16px;padding:20px}.study-form label{display:block;margin-bottom:8px;font-weight:750}.form-row{display:flex;gap:10px}.study-form input{min-width:0;flex:1;border:1px solid #bdc6de;border-radius:11px;padding:12px 14px}.study-form button{border:0;border-radius:11px;padding:12px 18px;color:#fff;background:#4f46e5;font-weight:750}.progress-card{display:grid;grid-template-columns:1fr 180px;align-items:center;gap:20px;margin-bottom:16px;padding:18px 20px}.progress-card div{display:flex;flex-direction:column;gap:3px}.progress-label,.section-heading span{color:#69738c;font-size:.82rem;font-weight:700;text-transform:uppercase}progress{width:100%;accent-color:#4f46e5}.topic-section{padding:20px}.section-heading{display:flex;align-items:center;justify-content:space-between;gap:16px;margin-bottom:14px}.section-heading h2{margin-bottom:0}.topic-list{display:grid;gap:10px}.study-item{display:grid;grid-template-columns:auto 1fr auto;align-items:center;gap:12px;border:1px solid #e0e5f2;border-radius:12px;padding:14px;background:#fff}.study-item input{width:18px;height:18px;accent-color:#4f46e5}.study-item.complete .topic-title{color:#7c8498;text-decoration:line-through}.status{border-radius:999px;padding:5px 9px;color:#4338ca;background:#e0e7ff;font-size:.78rem;font-weight:750}.study-item.complete .status{color:#166534;background:#dcfce7}@media(max-width:600px){.app-shell{padding:28px 0}.form-row{flex-direction:column}.progress-card{grid-template-columns:1fr}}

What do these styles do?

  • The page styles create the pale background plus the centered app-shell layout.
  • The shared card styles give the form plus progress area plus topic section the same visual treatment.
  • The topic selectors arrange each checkbox plus title plus status label into a single row.
  • The media query switches the form plus progress card to single-column layouts below 600px.
  • Save src/index.css by pressing Ctrl+S.
  • Return to the browser tab running the planner.

You'll see a centered Study Planner above three white cards. The form plus progress display plus topic rows now fit inside the responsive layout.

That visual transformation is complete. Your static component tree now looks like a usable study planner.

Planner still looks unstyled?

Confirm that you replaced the contents of src/index.css. Check that src/main.jsx still imports ./index.css.

Ask for help if the plain layout remains: help me diagnose why src/index.css is not styling my React study planner

✔️ Awesome, I've got everything!

Your responsive planner styles are saved in src/index.css.

ⓧ I'd like to double check the full code

@import url('data:text/css,');
:root{font-family:Inter,ui-sans-serif,system-ui,sans-serif;color:#172033;background:#eef2ff}*{box-sizing:border-box}body{margin:0;min-width:320px;min-height:100vh;background:radial-gradient(circle at top left,#c7d2fe 0,transparent 35%),#eef2ff}button,input{font:inherit}button{cursor:pointer}#root{min-height:100vh}.app-shell{width:min(100% - 32px,760px);margin:0 auto;padding:48px 0}.hero{margin-bottom:28px}.eyebrow{margin:0 0 8px;color:#4f46e5;font-size:.78rem;font-weight:800;letter-spacing:.12em;text-transform:uppercase}h1,h2,p{margin-top:0}h1{margin-bottom:8px;font-size:clamp(2.2rem,8vw,4rem);line-height:1}.hero>p:last-child{max-width:540px;color:#566079;font-size:1.05rem}.study-form,.progress-card,.topic-section{border:1px solid #d8def0;border-radius:18px;background:rgba(255,255,255,.92);box-shadow:0 16px 45px rgba(74,77,120,.1)}.study-form{margin-bottom:16px;padding:20px}.study-form label{display:block;margin-bottom:8px;font-weight:750}.form-row{display:flex;gap:10px}.study-form input{min-width:0;flex:1;border:1px solid #bdc6de;border-radius:11px;padding:12px 14px}.study-form button{border:0;border-radius:11px;padding:12px 18px;color:#fff;background:#4f46e5;font-weight:750}.progress-card{display:grid;grid-template-columns:1fr 180px;align-items:center;gap:20px;margin-bottom:16px;padding:18px 20px}.progress-card div{display:flex;flex-direction:column;gap:3px}.progress-label,.section-heading span{color:#69738c;font-size:.82rem;font-weight:700;text-transform:uppercase}progress{width:100%;accent-color:#4f46e5}.topic-section{padding:20px}.section-heading{display:flex;align-items:center;justify-content:space-between;gap:16px;margin-bottom:14px}.section-heading h2{margin-bottom:0}.topic-list{display:grid;gap:10px}.study-item{display:grid;grid-template-columns:auto 1fr auto;align-items:center;gap:12px;border:1px solid #e0e5f2;border-radius:12px;padding:14px;background:#fff}.study-item input{width:18px;height:18px;accent-color:#4f46e5}.study-item.complete .topic-title{color:#7c8498;text-decoration:line-through}.status{border-radius:999px;padding:5px 9px;color:#4338ca;background:#e0e7ff;font-size:.78rem;font-weight:750}.study-item.complete .status{color:#166534;background:#dcfce7}@media(max-width:600px){.app-shell{padding:28px 0}.form-row{flex-direction:column}.progress-card{grid-template-columns:1fr}}
Test the static form limitation

The form now looks ready for a new topic. Its current submission still uses the browser's default behavior.

Before you submit the form, do you think your new topic will remain in the list?

  • Enter Practice JavaScript functions in the New study topic field.
  • Select Add topic.

You'll see the page reload. The three original topics remain without Practice JavaScript functions.

That shortfall is intentional. You have proved that the static form cannot remember a new topic.

Your planner has the full visual foundation plus a clear limitation to solve. Next, you'll connect the form to state so new topics appear immediately.

Make the Form Add Topics

The planner now looks polished, but its form still forgets every topic after submission. That reload breaks the quick feedback loop a study planner needs.

React state keeps changing interface data inside the page. The text field will become a controlled form.

An event handler will stop the browser reload. An immutable array update will add each submitted topic.

In this step, get ready to:
  • Store the input value in title state through a controlled form.
  • Handle form submission without a page reload.
  • Append new topic objects through an immutable state update.
Wire the controlled form and topic state

The form needs one state value for its text. The app needs another state value for the topic collection.

  • Switch back to src/App.jsx in Visual Studio Code.
  • Press Ctrl+A to select the current file contents.
  • Replace the static planner logic by pasting this complete version:
import { useState } from 'react'

const initialTopics = [{ id: 1, title: 'Review JavaScript arrays', done: true }, { id: 2, title: 'Practice React components', done: false }, { id: 3, title: 'Style a responsive layout', done: false }]
let nextId = 4

function StudyForm({ onAdd }) { const [title, setTitle] = useState(''); function handleSubmit(event) { event.preventDefault(); const trimmedTitle = title.trim(); if (!trimmedTitle) return; onAdd(trimmedTitle); setTitle('') } return <form className="study-form" onSubmit={handleSubmit}><label htmlFor="topic">New study topic</label><div className="form-row"><input id="topic" type="text" value={title} onChange={(event) => setTitle(event.target.value)} placeholder="Example: Practice JavaScript functions" /><button type="submit">Add topic</button></div></form> }
function Progress({ completed, total }) { return <section className="progress-card" aria-label="Study progress"><div><span className="progress-label">Progress</span><strong>{completed} of {total} complete</strong></div><progress value={completed} max={total} /></section> }
function StudyItem({ topic }) { return <label className={`study-item ${topic.done ? 'complete' : ''}`}><input type="checkbox" checked={topic.done} readOnly /><span className="topic-title">{topic.title}</span><span className="status">{topic.done ? 'Done' : 'Active'}</span></label> }
export default function App() { const [topics, setTopics] = useState(initialTopics); const completedCount = topics.filter((topic) => topic.done).length; function handleAdd(title) { setTopics((currentTopics) => [...currentTopics, { id: nextId++, title, done: false }]) } return <main className="app-shell"><header className="hero"><p className="eyebrow">React practice project</p><h1>Study Planner</h1><p>Turn your learning goals into a focused checklist.</p></header><StudyForm onAdd={handleAdd} /><Progress completed={completedCount} total={topics.length} /><section className="topic-section" aria-labelledby="topic-heading"><div className="section-heading"><h2 id="topic-heading">Today's topics</h2><span>{topics.length} total</span></div><div className="topic-list">{topics.map((topic) => <StudyItem key={topic.id} topic={topic} />)}</div></section></main> }

What does this code do?

  • The title value keeps the text field synchronized with state.
  • handleSubmit() prevents the browser's default form reload.
  • trimmedTitle removes surrounding spaces before the topic is added.
  • handleAdd() receives the submitted title through onAdd.
  • ...currentTopics copies the existing topics into a fresh array before the new object is appended.
  • completedCount keeps the displayed progress based on the current topic collection.
  • Save src/App.jsx.
  • Return to the browser tab from earlier.

You should see the planner with its three original topic rows. This confirms the new state wiring compiles successfully.

Seeing a blank planner?

  • Confirm import { useState } from 'react' is the first line of src/App.jsx.
  • Confirm the entire static file was replaced so no older StudyForm or App code remains below the new version.
  • Compare my file with the complete version.

✔️ Awesome, I've got everything!

Your form state and topic state are now wired together.

ⓧ I'd like to double check the full code

import { useState } from 'react'

const initialTopics = [{ id: 1, title: 'Review JavaScript arrays', done: true }, { id: 2, title: 'Practice React components', done: false }, { id: 3, title: 'Style a responsive layout', done: false }]
let nextId = 4

function StudyForm({ onAdd }) { const [title, setTitle] = useState(''); function handleSubmit(event) { event.preventDefault(); const trimmedTitle = title.trim(); if (!trimmedTitle) return; onAdd(trimmedTitle); setTitle('') } return <form className="study-form" onSubmit={handleSubmit}><label htmlFor="topic">New study topic</label><div className="form-row"><input id="topic" type="text" value={title} onChange={(event) => setTitle(event.target.value)} placeholder="Example: Practice JavaScript functions" /><button type="submit">Add topic</button></div></form> }
function Progress({ completed, total }) { return <section className="progress-card" aria-label="Study progress"><div><span className="progress-label">Progress</span><strong>{completed} of {total} complete</strong></div><progress value={completed} max={total} /></section> }
function StudyItem({ topic }) { return <label className={`study-item ${topic.done ? 'complete' : ''}`}><input type="checkbox" checked={topic.done} readOnly /><span className="topic-title">{topic.title}</span><span className="status">{topic.done ? 'Done' : 'Active'}</span></label> }
export default function App() { const [topics, setTopics] = useState(initialTopics); const completedCount = topics.filter((topic) => topic.done).length; function handleAdd(title) { setTopics((currentTopics) => [...currentTopics, { id: nextId++, title, done: false }]) } return <main className="app-shell"><header className="hero"><p className="eyebrow">React practice project</p><h1>Study Planner</h1><p>Turn your learning goals into a focused checklist.</p></header><StudyForm onAdd={handleAdd} /><Progress completed={completedCount} total={topics.length} /><section className="topic-section" aria-labelledby="topic-heading"><div className="section-heading"><h2 id="topic-heading">Today's topics</h2><span>{topics.length} total</span></div><div className="topic-list">{topics.map((topic) => <StudyItem key={topic.id} topic={topic} />)}</div></section></main> }
Check blank submissions

The submit handler trims the current text before using it. Empty text stops at the guard clause so it never becomes a topic.

  • Clear the New study topic field.
  • Select Add topic.

You'll still see 3 total. The planner stays on the same page.

Add a topic without reloading

The onAdd prop carries the trimmed title from StudyForm to App. handleAdd() returns a fresh array containing the new topic object.

Before you add the topic, predict the total count and the input field's value after submission:

  • Enter Practice JavaScript functions in the New study topic field.
  • Select Add topic.

You'll see Practice JavaScript functions as a fourth row. The total changes to 4 total.

The input clears immediately. The page keeps its current session without reloading.

That is the form working end to end. A submitted topic now joins the planner immediately.

The topic list now grows from React state. Next, you'll connect the checkboxes to shared state so progress can respond.

Track Completion and Progress

Your planner already adds new topics without reloading the page. Its checkboxes still read each topic's status without changing the React state behind the interface.

Completion needs a path from each StudyItem back to App. In this step, you will update one topic object immutably. The existing count then becomes live progress feedback.

In this step, get ready to:
  • Connect each checkbox to an event handler through props.
  • Flip the selected topic's done value with an immutable array update.
  • Render completion feedback from the current topics state.
Connect each checkbox to shared state

The StudyItem checkbox is controlled by topic.done. The new onToggle callback gives the component a route back to App.

Inside App, handleToggle uses map() to create the next topics array. Only the matching topic receives a new object with its completion value flipped.

  • Update the completion path in src/App.jsx with the matching snippets below:
function StudyItem({ topic, onToggle }) {
  return (
    <label className={`study-item ${topic.done ? 'complete' : ''}`}>
      <input
        type="checkbox"
        checked={topic.done}
        onChange={() => onToggle(topic.id)}
      />
      <span className="topic-title">{topic.title}</span>
      <span className="status">{topic.done ? 'Done' : 'Active'}</span>
    </label>
  )
}

  function handleToggle(id) {
    setTopics((currentTopics) =>
      currentTopics.map((topic) =>
        topic.id === id ? { ...topic, done: !topic.done } : topic,
      ),
    )
  }

          {topics.map((topic) => (
            <StudyItem key={topic.id} topic={topic} onToggle={handleToggle} />
          ))}

How Does the Completion Path Work?

  • StudyItem accepts the onToggle callback from its parent.
  • checked keeps the checkbox aligned with topic.done.
  • onChange sends the selected topic.id back to App.
  • handleToggle passes an updater function to setTopics.
  • map() preserves every unmatched topic object. The matching object is copied with spread syntax. Its done value is flipped.
  • Save src/App.jsx.
  • Return to the browser from earlier.
  • Refresh the page.
  • Select the checkbox beside Practice React components.

You'll see the row change from Active to Done. The progress count increases by one.

That connection is live. A click in the child component now updates the shared topics array.

Checkbox Still Does Not Change?

Check that the mapped StudyItem receives onToggle={handleToggle}. Confirm that the checkbox uses onChange={() => onToggle(topic.id)}.

Make sure handleToggle sits inside App after handleAdd.

Help me trace why the study planner checkbox does not update its topic state.

Show progress and completion feedback

The existing completedCount filters the current topics array every time state changes. This keeps the progress display derived from the same source of truth.

Conditional rendering can use that count to reveal a message at the exact moment every topic is complete.

  • In src/App.jsx, find the line that renders Progress.
  • Add the completion message directly below that line with the code below:
{completedCount === topics.length && (
  <p className="success-message">Study plan complete. Great work!</p>
)}

Why Does This Message Stay Accurate?

The condition compares the number of completed topics with the total number of topics. React renders the message only when both values match.

Adding another topic increases topics.length. The message then disappears until that new topic is complete.

Before you run the final check, what do you expect to happen when you select the last unfinished checkbox?

  • Save src/App.jsx.
  • Return to the browser.
  • Refresh the page.
  • Select the unfinished checkboxes until every row shows Done.

You'll see every row marked Done.

The progress display reaches the full topic count. The message Study plan complete. Great work! appears above the topic list.

Well done. Your planner now derives every completion view from the same topics array.

Completion Message Missing?

Confirm that the conditional block sits directly below the Progress component. Check that the comparison uses completedCount === topics.length.

If one row still shows Active, select its checkbox before checking for the message.

Help me find why the completion message does not appear when every study topic is done.

✔️ Awesome, I've got everything!

Your src/App.jsx file is ready. Make sure it is saved.

ⓧ I'd like to double check the full code

import { useState } from 'react'

const initialTopics = [{ id: 1, title: 'Review JavaScript arrays', done: true }, { id: 2, title: 'Practice React components', done: false }, { id: 3, title: 'Style a responsive layout', done: false }]
let nextId = 4
function StudyForm({ onAdd }) { const [title, setTitle] = useState(''); function handleSubmit(event) { event.preventDefault(); const trimmedTitle = title.trim(); if (!trimmedTitle) return; onAdd(trimmedTitle); setTitle('') } return <form className="study-form" onSubmit={handleSubmit}><label htmlFor="topic">New study topic</label><div className="form-row"><input id="topic" type="text" value={title} onChange={(event) => setTitle(event.target.value)} placeholder="Example: Practice JavaScript functions" /><button type="submit">Add topic</button></div></form> }
function Progress({ completed, total }) { return <section className="progress-card" aria-label="Study progress"><div><span className="progress-label">Progress</span><strong>{completed} of {total} complete</strong></div><progress value={completed} max={total} /></section> }
function StudyItem({ topic, onToggle }) { return <label className={`study-item ${topic.done ? 'complete' : ''}`}><input type="checkbox" checked={topic.done} onChange={() => onToggle(topic.id)} /><span className="topic-title">{topic.title}</span><span className="status">{topic.done ? 'Done' : 'Active'}</span></label> }
export default function App() { const [topics, setTopics] = useState(initialTopics); const completedCount = topics.filter((topic) => topic.done).length; function handleAdd(title) { setTopics((currentTopics) => [...currentTopics, { id: nextId++, title, done: false }]) } function handleToggle(id) { setTopics((currentTopics) => currentTopics.map((topic) => topic.id === id ? { ...topic, done: !topic.done } : topic)) } return <main className="app-shell"><header className="hero"><p className="eyebrow">React practice project</p><h1>Study Planner</h1><p>Turn your learning goals into a focused checklist.</p></header><StudyForm onAdd={handleAdd} /><Progress completed={completedCount} total={topics.length} />{completedCount === topics.length && <p className="success-message">Study plan complete. Great work!</p>}<section className="topic-section" aria-labelledby="topic-heading"><div className="section-heading"><h2 id="topic-heading">Today's topics</h2><span>{topics.length} total</span></div><div className="topic-list">{topics.map((topic) => <StudyItem key={topic.id} topic={topic} onToggle={handleToggle} />)}</div></section></main> }

Your core study planner is complete. Next, an optional Secret Mission lets you push the same state-driven approach further.

Secret mission

Filter the Study List

A growing study plan becomes harder to scan when finished topics sit beside unfinished work. Add All, Active, and Done filters that derive each view from the existing topic data. You will also show a clear message when a filter has no matching topics.

Clean Up Your Resources

Clean Up Your Resources

Choose whether to keep your local planner available, pause its development server, or delete the project entirely. Everything runs on your computer, so keeping the project has no ongoing financial cost.

Resources you used:

  • A local Vite development server running the planner from the react-study-planner folder.
  • The react-study-planner folder containing your React source files and installed dependencies.

Keep everything running

No action is needed. Choose this option if you want to keep testing or improving your study planner now.

  • Leave the Vite development server running while you continue testing the planner.
  • Keep the react-study-planner folder for future changes.
  • Return to the browser at http://localhost:5173 whenever you want to use the running app.

Pause - I'll come back to this later

Shut down the local development server to free up memory. Your project files remain ready for another session.

  • Switch back to the PowerShell terminal in Visual Studio Code.
  • Press Ctrl+C to stop the Vite development server.
  • Confirm the PowerShell prompt returns.
  • Keep the react-study-planner folder so you can restart the project later.

Delete - I don't want to use this again

Removing the project folder permanently deletes the planner and its installed dependencies. Your installed development tools remain available for other projects.

  • Switch back to the PowerShell terminal where Vite is running.
  • Press Ctrl+C to stop the development server.
  • Move above the project folder and remove it by running these PowerShell commands:
cd ..
Remove-Item -Recurse -Force react-study-planner

What Do These Commands Remove?

The first command moves PowerShell to the directory containing your project. The second command permanently removes the entire react-study-planner folder.

  • Confirm the command returns to the PowerShell prompt without an error.
  • Close the browser tab showing http://localhost:5173.

Folder Still in Use?

  • Press Ctrl+C again if the development server is still printing messages.
  • Run the deletion commands from the directory directly above react-study-planner.
  • Help me remove the local project folder.

Nice Work!

Nice Work!

That's your React study planner complete. It now responds to every topic you add or complete.

Its progress display stays in sync with your topics. Its filtered views use the same data.

You've learned how to:

  • Build a responsive study planner from reusable components written in JSX.
  • Turn a static form into a controlled form that adds topics instantly without reloading the page.
  • Apply immutable array updates to topic objects. Derive progress from the same source of truth.
  • Secret Mission: Add All, Active, or Done filter views while keeping topics as the single source of truth.

Ready to quiz yourself?