Build a Tech Career Pathfinder

Build a React app to explore tech career paths and track skill progress.

Introduction

30 Second Summary

When you're starting in tech, the sheer number of possible career paths can feel overwhelming. Frontend, DevOps, cybersecurity. How do you know which skills to learn first, and how do you track your progress along the way?

In this project, you will build a Tech Career Pathfinder app using React. Users pick a specialization, view a structured learning roadmap for that path, and check off skills as they go.

What You'll Build

When you open the finished app, you'll see three career path cards. Click one and you land on a detailed roadmap of skills to learn. Check them off, close the browser, come back a week later, and your progress is still there.

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

  • Three specialization cards that you can click to explore Frontend Development, DevOps, or Cybersecurity roadmaps.
  • A detailed roadmap view that breaks each career into learning stages with skills you can check off as you learn them.
  • Persistent progress tracking that survives page refreshes and browser restarts using localStorage.
  • Secret Mission: Add a visual progress bar that shows your completion percentage for each specialization.

Are there any prerequisites?

You need a basic understanding of HTML and JavaScript syntax (variables, functions, arrays, objects). No prior React experience is needed.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters. This step is your chance to commit to the project goals before any hands-on work begins.

Set Up Your React Project with Vite

To build our Tech Career Pathfinder, we need a working development environment that lets us see changes instantly as we code.

We will use Vite, a modern build tool that gives you instant hot-reload during development. Every time you save a file, the browser updates automatically without a manual refresh.

In this step, get ready to:
  • Scaffold a new React project using Vite.
  • Install project dependencies and start the dev server.
  • Verify the starter page loads and customize the project files.
Scaffold the React project

Vite provides a scaffold command that generates all the starter files for a React app in one step.

  • In VS Code, open a new terminal by clicking Terminal in the top menu bar, then New Terminal.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your React project by running this command:
npm create vite@latest pathfinder -- --template react

What will happen when you run this?

This command triggers a few interactive prompts. Here is what to expect:

  • npm asks Need to install the following packages: create-vite@... Ok to proceed? (y) . Press y then Enter.
  • Vite asks Use rolldown-vite (Experimental)? . Select No (the default) and press Enter.
  • Vite asks Install with npm and start now? . Select No and press Enter. We will install and start manually so you can see each step.
  • After the prompts finish, confirm the project folder was created by running:
ls

You should see a pathfinder folder listed in the output.

Don't see the pathfinder folder?

  • Make sure you pressed y at the first prompt. If you accidentally declined, re-run the scaffold command.
  • Check that you are on your Desktop. Run pwd to see your current directory.

Need more help? Why didn't my Vite scaffold command create a project folder?

Install dependencies and start the dev server

The scaffold created the project structure, but the packages your app needs are not yet downloaded.

  • Move into the project folder and install its dependencies by running these commands:
cd pathfinder
npm install

You should see output listing the installed packages, ending with a line like added X packages.

  • Start the development server by running:
npm run dev

What does this command do?

This starts a local web server that serves your React app. It also watches for file changes and automatically refreshes the browser whenever you save. The terminal will display a local URL where your app is running.

Your terminal should display a URL: http://localhost:5173.

Not seeing the localhost URL?

  • Make sure you ran npm install before npm run dev. Without installed packages, the dev server cannot start.
  • Check that you are inside the pathfinder folder. Run pwd and confirm the path ends with /pathfinder.

Still stuck? Help me fix my Vite dev server

Verify the starter page and customize your project

Before you open the browser, what do you think you will see at that URL? The project was just scaffolded from a template, so think about what a starter page might show.

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

You should see the default Vite + React starter page with a counter button. Clicking the button increments the count.

Now let's personalize the project files for our Tech Career Pathfinder app.

  • In the VS Code Explorer sidebar, expand the pathfinder folder and open index.html.
  • Find this line:
<title>Vite + React</title>
  • Replace it with:
<title>Tech Career Pathfinder</title>
  • Save index.html.
  • Check your browser tab. It should now say Tech Career Pathfinder instead of Vite + React.
  • Now open src/main.jsx in the Explorer sidebar.
  • Replace the entire contents of the file with the following code:
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.jsx'
import './App.css'

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

What does this code do?

  • It imports React's StrictMode wrapper and the createRoot function that mounts your app into the page.
  • It imports App.jsx (your main component) and App.css (global styles). We consolidate the CSS import here because later steps will replace both files entirely.
  • The createRoot call finds the <div id="root"> element in index.html and renders your React app inside it.
  • Save src/main.jsx.
  • Check your browser. The page should still show the starter content with the counter button. The app still works because we have not changed what App.jsx renders.

Seeing an error in the browser?

  • Make sure you saved the file. Unsaved changes do not trigger a reload.
  • Check that the import path says './App.css' with a capital A. File names are case-sensitive.
  • If you see a blank page, open your browser's developer console (Cmd+Option+J on macOS or Ctrl+Shift+J on Windows) and check the error message.

Still seeing issues? Help me fix my main.jsx file

✔️ Awesome, I've got everything!

Great. Make sure both files are saved and your browser tab reads Tech Career Pathfinder.

ⓧ I'd like to double check the full code

Here is what index.html should look like:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Tech Career Pathfinder</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.jsx"></script>
  </body>
</html>

And here is what src/main.jsx should look like:

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

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

Your development environment is ready and running. Next up, you will replace the starter page with real specialization cards that users can click.

Build the Specialization Selection Page

Your dev server is running and the starter page proves everything works. Now it is time to replace that placeholder with the landing page of your app.

The landing page is the first thing users see. Building it first gives you a visible working app within minutes and teaches you how React components accept data through props.

In this step, get ready to:
  • Create a data file that stores career path information for three specializations.
  • Build reusable React components that display specialization cards.
  • Style the card grid layout with CSS.
Create the roadmap data file

Every card on the landing page needs a title, description, and a list of skills organized by stage. You will store all of this in a single JavaScript file that the rest of the app imports.

  • Create both folders your app needs by running this command in your VS Code terminal:
mkdir src/data src/components
  • Create a new file called roadmaps.js inside src/data.
  • Open src/data/roadmaps.js in VS Code and paste the following to start the array with the Frontend Developer specialization:
const roadmaps = [
  {
    id: "frontend",
    title: "Frontend Developer",
    description:
      "A frontend developer uses HTML, CSS, and JavaScript to design and build the visual and interactive elements of websites and applications that users engage with directly.",
    stages: [
      {
        name: "Foundations",
        skills: [
          { id: "fe-html", label: "HTML" },
          { id: "fe-css", label: "CSS" },
          { id: "fe-javascript", label: "JavaScript" },
        ],
      },
      {
        name: "Core Skills",
        skills: [
          { id: "fe-responsive", label: "Responsive Design" },
          { id: "fe-accessibility", label: "Accessibility" },
          { id: "fe-git", label: "Git Version Control" },
        ],
      },
  • Continue pasting directly below to finish the Frontend path and start the DevOps specialization:
      {
        name: "Professional",
        skills: [
          { id: "fe-react", label: "React" },
          { id: "fe-testing", label: "Testing and Debugging" },
          { id: "fe-performance", label: "Web Performance" },
          { id: "fe-typescript", label: "TypeScript" },
        ],
      },
    ],
  },
  {
    id: "devops",
    title: "DevOps Engineer",
    description:
      "A DevOps Engineer specializes in the practices and tools that enable continuous delivery of software, bridging the gap between development and operations teams.",
    stages: [
      {
        name: "Foundations",
        skills: [
          { id: "do-linux", label: "Linux Fundamentals" },
          { id: "do-scripting", label: "Scripting (Python or Bash)" },
          { id: "do-networking", label: "Networking Basics" },
        ],
      },
  • Continue pasting below to finish the DevOps path and start the Cybersecurity specialization:
      {
        name: "Core Skills",
        skills: [
          { id: "do-git", label: "Git and GitHub" },
          { id: "do-cicd", label: "CI/CD Pipelines" },
          { id: "do-containers", label: "Docker Containers" },
        ],
      },
      {
        name: "Professional",
        skills: [
          { id: "do-kubernetes", label: "Kubernetes" },
          { id: "do-cloud", label: "Cloud Platforms (AWS)" },
          { id: "do-terraform", label: "Terraform" },
          { id: "do-monitoring", label: "Monitoring and Logging" },
        ],
      },
    ],
  },
  {
    id: "cybersecurity",
    title: "Cybersecurity Expert",
    description:
      "A Cybersecurity Expert protects systems, networks, and programs from digital attacks, focusing on identifying vulnerabilities and implementing defense strategies.",
    stages: [
  • Finish the file by pasting the Cybersecurity stages and the closing export:
      {
        name: "Foundations",
        skills: [
          { id: "cs-networking", label: "Networking and Protocols" },
          { id: "cs-os", label: "Operating Systems" },
          { id: "cs-linux", label: "Linux Administration" },
        ],
      },
      {
        name: "Core Skills",
        skills: [
          { id: "cs-cryptography", label: "Cryptography" },
          { id: "cs-security-principles", label: "Security Principles" },
          { id: "cs-threats", label: "Common Threats and Attacks" },
        ],
      },
      {
        name: "Professional",
        skills: [
          { id: "cs-pentesting", label: "Penetration Testing" },
          { id: "cs-incident", label: "Incident Response" },
          { id: "cs-compliance", label: "Compliance and Governance" },
          { id: "cs-cloud-security", label: "Cloud Security" },
        ],
      },
    ],
  },
];

export default roadmaps;

What does this data structure do?

Each specialization is an object with an id, a title, a description, and a stages array.

Each stage groups related skills together (Foundations, Core Skills, Professional). Every skill has a unique id for tracking progress later and a human-readable label for display.

  • Save src/data/roadmaps.js.
  • Confirm the file exists by running:
ls src/data

You should see roadmaps.js listed in the output.

Don't see the file?

Make sure you created the data folder inside src (not at the project root). Your file path should be src/data/roadmaps.js.

Help me find my roadmaps file

Build the App and SpecializationCard components

React apps are built from components, each responsible for one piece of UI. You will create a SpecializationCard component that renders a single card, then wire it into App.jsx which renders one card for each specialization in the data.

  • Create a new file at src/components/SpecializationCard.jsx and paste this component:
function SpecializationCard({ title, description, onClick }) {
  return (
    <div className="card" onClick={onClick}>
      <h2>{title}</h2>
      <p>{description}</p>
    </div>
  );
}

export default SpecializationCard;

What are props?

The curly braces in the function signature ({ title, description, onClick }) destructure the props object. Props are data passed from a parent component to a child.

This component doesn't know where its data comes from. It just renders whatever title and description it receives, making it reusable for any specialization.

  • Open src/App.jsx and replace its entire contents with the following:
import SpecializationCard from './components/SpecializationCard.jsx';
import roadmaps from './data/roadmaps.js';

function App() {
  return (
    <div className="app">
      <header>
        <h1>Tech Career Pathfinder</h1>
        <p className="subtitle">
          Choose a specialization and track your learning journey
        </p>
      </header>
      <main>
        <div className="card-grid">
          {roadmaps.map((roadmap) => (
            <SpecializationCard
              key={roadmap.id}
              title={roadmap.title}
              description={roadmap.description}
              onClick={() => {}}
            />
          ))}
        </div>
      </main>
    </div>
  );
}

export default App;

What does this code do?

  • roadmaps.map(...) loops through the array of specializations and creates one SpecializationCard for each.
  • key={roadmap.id} gives React a unique identifier for each card so it can efficiently update the list.
  • onClick={() => {}} passes an empty function for now. You will wire up real navigation in the next step.
  • Save both SpecializationCard.jsx and App.jsx.
  • Check your browser at http://localhost:5173. You should see three card titles (Frontend Developer, DevOps Engineer, Cybersecurity Expert) with descriptions, rendered as plain unstyled text.

Seeing a blank page or error?

  • Check the terminal running your dev server for a red error message. A common issue is a typo in the import path. Make sure './components/SpecializationCard.jsx' matches your file name exactly (case-sensitive).
  • Confirm roadmaps.js is inside src/data/ and that the import path says './data/roadmaps.js'.
  • Make sure roadmaps.js ends with export default roadmaps; and the array brackets are balanced.

Help me debug my blank page

Style the card grid

The cards work but they look like bare text on a white background. You will replace the default CSS with a dark theme and a responsive grid layout.

  • Open src/App.css and replace its entire contents with the following global and layout styles:
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  background-color: #0f172a;
  color: #e2e8f0;
  min-height: 100vh;
}

.app {
  max-width: 900px;
  margin: 0 auto;
  padding: 2rem 1rem;
}

header {
  text-align: center;
  margin-bottom: 2rem;
}

header h1 {
  font-size: 2rem;
  color: #f8fafc;
  margin-bottom: 0.5rem;
}
  • Continue pasting the subtitle and card grid styles directly below:

.subtitle {
  color: #94a3b8;
  font-size: 1.1rem;
}

.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  gap: 1.5rem;
}

.card {
  background-color: #1e293b;
  border: 1px solid #334155;
  border-radius: 12px;
  padding: 1.5rem;
  cursor: pointer;
  transition: transform 0.2s, border-color 0.2s;
}
  • Finish the file by pasting the card hover and text styles below:

.card:hover {
  transform: translateY(-4px);
  border-color: #60a5fa;
}

.card h2 {
  font-size: 1.3rem;
  color: #f8fafc;
  margin-bottom: 0.75rem;
}

.card p {
  color: #94a3b8;
  font-size: 0.9rem;
  line-height: 1.5;
}

What do these styles do?

  • .card-grid uses CSS Grid to lay cards out in a responsive row that wraps on smaller screens.
  • .card:hover lifts the card up slightly and highlights the border when you mouse over it.
  • cursor: pointer tells the user the card is clickable, even though the click does nothing yet.

Before you check the browser, what do you think the page looks like now?

  • Save src/App.css and check your browser.

You should see three dark cards on a navy background, each showing a specialization title and description. Hover over a card to see it lift up with a blue border highlight.

Cards not styled?

  • Make sure you replaced ALL the content in App.css. Leftover Vite starter styles can conflict with your new layout.
  • Confirm the className values in your JSX match the CSS selectors exactly: "app", "card-grid", and "card".
  • Check that main.jsx still has the line import './App.css' at the top.

Help me fix my card styling

  • Try clicking one of the cards.

Nothing happens. That is expected. The onClick handler is an empty function right now. You will add real navigation in the next step.

✔️ Awesome, I've got everything!

Great. Double check you have saved all your files before moving on.

ⓧ I'd like to double check the full code

Scroll up and confirm your files contain each chunk in order:

  • src/data/roadmaps.js has all three specialization objects (frontend, devops, cybersecurity) inside the roadmaps array, followed by export default roadmaps;.
  • src/components/SpecializationCard.jsx defines a function component that destructures title, description, and onClick from props.
  • src/App.jsx imports SpecializationCard and roadmaps, then maps over the data to render three cards.
  • src/App.css starts with the global reset and body styles, followed by .app, header, .subtitle, .card-grid, and .card blocks.

Your landing page is alive. Next up, you will make those cards actually do something by adding view switching and a full roadmap display.

Add View Switching and Roadmap Display

Your three cards look great, but clicking them does nothing. Users need a way to drill into a specialization and see its full learning roadmap.

In this step you will add conditional rendering, the core React pattern for showing different UI based on state. A single piece of state decides whether the user sees the card grid or a detailed roadmap view.

In this step, get ready to:
  • Create a RoadmapView component that displays skill stages for a career path.
  • Add state to App.jsx that tracks which specialization is selected.
  • Wire up conditional rendering so clicking a card shows the roadmap and a back button returns to the selection.
Create the RoadmapView component

The roadmap view is what users see after clicking a card. It receives a specialization's data and renders each learning stage as a section with a list of skills.

  • Create a new file at src/components/RoadmapView.jsx and paste this first section of code:
function RoadmapView({ roadmap, progress, onToggleSkill, onBack }) {
  return (
    <div className="roadmap-view">
      <button className="back-button" onClick={onBack}>
        Back to Specializations
      </button>
      <h1>{roadmap.title}</h1>
      <p className="roadmap-description">{roadmap.description}</p>
      {roadmap.stages.map((stage) => (
        <div key={stage.name} className="stage">
          <h3>{stage.name}</h3>
          <ul className="skills-list">
  • Paste this code directly below to render the skill checkboxes inside each stage:
            {stage.skills.map((skill) => (
              <li key={skill.id} className="skill-item">
                <label>
                  <input
                    type="checkbox"
                    checked={progress[skill.id] || false}
                    onChange={() => onToggleSkill(skill.id)}
                  />
                  <span
                    className={progress[skill.id] ? "completed" : ""}
                  >
                    {skill.label}
                  </span>
                </label>
              </li>
            ))}
          </ul>
        </div>
      ))}
    </div>
  );
}

export default RoadmapView;

What does this code do?

This component receives four props: the roadmap data, a progress object, a toggle function, and a back handler.

  • It maps over roadmap.stages to create a section for each learning stage (Foundations, Core Skills, Professional).
  • Inside each stage, it maps over the skills array to render a checkbox and label for every skill.
  • The progress and onToggleSkill props are placeholders for now. They become functional in Step 4.
  • Save src/components/RoadmapView.jsx.
  • Confirm you see RoadmapView.jsx in the Explorer sidebar under src/components/.
Add state and conditional rendering to App.jsx

React's useState hook lets a component remember a value between renders. You will track which card the user clicked, then conditionally render either the card grid or the RoadmapView.

  • Open src/App.jsx and replace its entire contents. Start with this section containing the imports and state setup:
import { useState } from 'react';
import SpecializationCard from './components/SpecializationCard.jsx';
import RoadmapView from './components/RoadmapView.jsx';
import roadmaps from './data/roadmaps.js';

function App() {
  // Track which specialization the user selected
  const [selectedPath, setSelectedPath] = useState(null);

  // Find the full roadmap object for the selected ID
  const selectedRoadmap = roadmaps.find((r) => r.id === selectedPath);

  return (
    <div className="app">
      <header>
        <h1>Tech Career Pathfinder</h1>
        <p className="subtitle">
          Choose a specialization and track your learning journey
        </p>
      </header>
  • Paste this code directly below to add the conditional rendering logic and close the component:
      <main>
        {selectedRoadmap ? (
          <RoadmapView
            roadmap={selectedRoadmap}
            progress={{}}
            onToggleSkill={() => {}}
            onBack={() => setSelectedPath(null)}
          />
        ) : (
          <div className="card-grid">
            {roadmaps.map((roadmap) => (
              <SpecializationCard
                key={roadmap.id}
                title={roadmap.title}
                description={roadmap.description}
                onClick={() => setSelectedPath(roadmap.id)}
              />
            ))}
          </div>
        )}
      </main>
    </div>
  );
}

export default App;

What is conditional rendering?

The ternary expression selectedRoadmap ? (...) : (...) is conditional rendering. If a roadmap is selected, React renders the RoadmapView. Otherwise it renders the card grid.

  • useState(null) creates a state variable that starts as null (no card selected yet). Calling setSelectedPath triggers a re-render.
  • Each card's onClick now calls setSelectedPath(roadmap.id), switching the view to RoadmapView.
  • The back button calls setSelectedPath(null), returning the user to the card grid.
  • Save src/App.jsx.
  • Switch to your browser and click any specialization card.

The cards disappear and the roadmap view appears with stage headings and skill names. The view switching works. Click the back button to return to the cards.

Seeing a blank page or error?

  • Check that both code blocks are pasted together as one continuous file. The second block starts directly after the closing </header> tag.
  • Confirm the file src/components/RoadmapView.jsx exists. The import in App.jsx will fail if the file is missing.
  • Make sure you have import { useState } from 'react'; at the very top of App.jsx.

Still stuck? Help me debug my App.jsx conditional rendering

Style the roadmap view

The view switching works but the roadmap content is unstyled. Time to add CSS for the layout, back button, stage cards, and skill list.

  • Open src/App.css and add these styles at the bottom of the file for the roadmap layout and navigation:
.roadmap-view {
  max-width: 700px;
  margin: 0 auto;
}

.back-button {
  background-color: #334155;
  color: #e2e8f0;
  border: none;
  padding: 0.5rem 1rem;
  border-radius: 8px;
  cursor: pointer;
  font-size: 0.9rem;
  margin-bottom: 1.5rem;
  transition: background-color 0.2s;
}

.back-button:hover {
  background-color: #475569;
}

.roadmap-view h1 {
  font-size: 1.8rem;
  color: #f8fafc;
  margin-bottom: 0.5rem;
}

What does this code do?

  • .roadmap-view constrains the content width and centers it on the page.
  • .back-button styles the navigation button with a slate background, rounded corners, and a hover color shift.
  • The heading rule matches the dark theme established in the card grid.
  • Save src/App.css and click into any specialization in the browser.

You should see the back button styled as a rounded slate pill and a large white title. The skill lists below are still raw.

  • Add the stage card styles directly below the code you just pasted:
.roadmap-description {
  color: #94a3b8;
  margin-bottom: 2rem;
  line-height: 1.5;
}

.stage {
  margin-bottom: 1.5rem;
  background-color: #1e293b;
  border-radius: 12px;
  padding: 1.25rem;
}

.stage h3 {
  font-size: 1.1rem;
  color: #60a5fa;
  margin-bottom: 0.75rem;
}

.skills-list {
  list-style: none;
}

.skill-item {
  padding: 0.5rem 0;
  border-bottom: 1px solid #334155;
}

.skill-item:last-child {
  border-bottom: none;
}

What does this code do?

  • .stage gives each learning stage a dark card-like background with rounded corners, visually grouping its skills.
  • .stage h3 uses the blue accent color to differentiate stage names from other text.
  • The skill item rules remove default list bullets and add subtle divider lines between skills.
  • Save and check the browser.

Each stage now appears as its own dark card with a blue heading. Skills are listed with divider lines between them.

  • Add the final set of styles below to finish the skill item layout:
.skill-item label {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  cursor: pointer;
  font-size: 0.95rem;
}

.skill-item input[type="checkbox"] {
  width: 18px;
  height: 18px;
  accent-color: #60a5fa;
  cursor: pointer;
}

.completed {
  text-decoration: line-through;
  color: #64748b;
}

What does this code do?

  • The label rule uses flexbox to align the checkbox and text side by side with consistent spacing.
  • accent-color: #60a5fa tints the browser's default checkbox to match the blue accent theme.
  • .completed will strike through skill text once progress tracking is added in the next step.
  • Save src/App.css.

Before you test the full flow, what do you think will happen when you click a card now?

  • Click any specialization card in the browser.

The cards disappear and a fully styled roadmap view appears. You see the specialization title, description, and skill stages organized as dark cards with blue headings. Each skill has a checkbox beside it. Click the back button to return to the three cards.

Checkboxes or labels look misaligned?

  • Verify the selector is .skill-item label (with a space, not a dot). The space means "a label inside a skill-item".
  • Check that display: flex and align-items: center are both present in that rule.

Need more help? Help me fix my roadmap CSS styling

✔️ Awesome, I've got everything!

Great. Double check you have saved both src/App.jsx and src/App.css.

ⓧ I'd like to double check the full code

Your src/components/RoadmapView.jsx should contain both code chunks you pasted above, in order:

  • The function signature function RoadmapView({ roadmap, progress, onToggleSkill, onBack }) at the top.
  • A back button, h1, and description paragraph.
  • The roadmap.stages.map() rendering each stage with its skills list and checkboxes.
  • export default RoadmapView; at the bottom.

Your src/App.jsx should contain both code chunks in order:

  • Four imports: useState, SpecializationCard, RoadmapView, roadmaps.
  • The selectedPath state and selectedRoadmap lookup inside the function.
  • The ternary in the return that renders RoadmapView or the card grid.
  • Each card's onClick calls setSelectedPath(roadmap.id).

Your src/App.css should have the three new style groups appended after the existing card styles:

  • Layout and navigation styles (.roadmap-view through .roadmap-view h1).
  • Stage and skill list styles (.roadmap-description through .skill-item:last-child).
  • Label and checkbox styles (.skill-item label through .completed).

Your cards now navigate into a detailed roadmap. Next up, you will make those checkboxes actually track your progress.

Add Progress Tracking with Checkboxes

Your app lets users browse career paths, but they have no way to track which skills they have already learned.

In this step, you will add state to the App component that tracks which checkboxes are checked. The checkboxes will work perfectly during a session, but you will discover a critical limitation at the end.

In this step, get ready to:
  • Add progress state and a toggle handler to the App component.
  • Understand how checkbox state flows through props to RoadmapView.
  • Discover what happens to in-memory state when you refresh the page.
Add progress state and handler to App.jsx

Right now, App.jsx passes an empty object and a no-op function as the progress props. You need real state that remembers which skills the user has checked.

  • In src/App.jsx, find this line:
const [selectedPath, setSelectedPath] = useState(null);
  • Add the progress state and handler function directly below it:
  const [progress, setProgress] = useState({});

  function handleToggleSkill(skillId) {
    setProgress((prev) => ({
      ...prev,
      [skillId]: !prev[skillId],
    }));
  }

What does this code do?

The progress state is a plain object where each key is a skill ID (like fe-html) and each value is true or false.

The handleToggleSkill function takes a skill ID and flips its value. It uses the spread operator (...prev) to copy all existing progress, then overrides the one skill that was toggled.

Now you need to connect this real state to the RoadmapView component instead of the placeholder values.

  • In the same file, scroll down to where RoadmapView is rendered. Find these two lines:
            progress={{}}
            onToggleSkill={() => {}}
  • Replace them with the real state and handler:
            progress={progress}
            onToggleSkill={handleToggleSkill}
  • Save src/App.jsx.
  • Switch to your browser at http://localhost:5173. Click any specialization card, then click a checkbox next to a skill. The checkbox fills in and the skill text gets a strikethrough.

Checkboxes not responding to clicks?

  • Make sure you replaced both progress={{}} and onToggleSkill={() => {}}. If either placeholder remains, the checkboxes will not respond.
  • Check that handleToggleSkill is defined inside the App function, not outside it.
  • help me debug why my checkboxes are not responding in RoadmapView
Understand how checkbox state flows through props

You did not need to change RoadmapView.jsx at all. It was already built to receive progress and onToggleSkill as props and wire them to the checkboxes.

How does the data flow work?

  • App owns the progress state and passes it down to RoadmapView as a prop.
  • Each checkbox reads progress[skill.id] to know if it should be checked.
  • When a user clicks a checkbox, onChange calls onToggleSkill(skill.id), which reaches back up to App and flips that skill's value in state.
  • React re-renders the component with the updated state, and the checkbox appears checked.

This pattern of "state lives in the parent, child communicates via callbacks" is one of the most common React patterns you will use.

Test persistence and discover the limitation

Your checkboxes work within a session. Let's test whether progress survives navigation and page refreshes.

  • Click into any specialization and check off 3 or 4 skills.
  • Click the Back to Specializations button to return to the card selection.
  • Click the same specialization card again to re-enter that roadmap.

Your checked skills are still checked. The state persists in memory as long as the app is running.

Now for the real test. Before you do the next action, predict what will happen: do you think your checked skills will survive a full page refresh?

  • Press Cmd+R (macOS) or Ctrl+R (Windows) to refresh the browser page.
  • Click into the same specialization you were tracking.

Every checkbox is empty. All your progress is gone.

Why did the progress disappear?

React state lives in memory. When you refresh the page, the browser destroys the entire JavaScript runtime and starts fresh. The progress object resets to {} because that is what useState({}) initializes to.

For a progress tracker to be useful, it needs to remember data between sessions. You will fix this in the next step.

✔️ Awesome, I've got everything!

Great. Make sure you have saved src/App.jsx.

ⓧ I'd like to double check the full code

The only file you changed in this step is src/App.jsx. Scroll up and confirm your file has:

  • const [progress, setProgress] = useState({}); on the line after your selectedPath state.
  • The handleToggleSkill function defined below the state declarations.
  • progress={progress} and onToggleSkill={handleToggleSkill} passed to RoadmapView (not the old empty object and no-op).

No other files changed in this step. RoadmapView.jsx already had the checkbox wiring from the previous step.

You have proven that in-memory state is not enough for a progress tracker. Next up, you will plug in localStorage so that checked skills survive page refreshes and even browser restarts.

Persist Progress with localStorage

In the last step, you saw the problem firsthand. Checkboxes work perfectly during a session, but the moment you refresh the page, everything resets to empty.

The browser's localStorage API solves this by letting you save data permanently on the user's machine. In this step, you will wire up localStorage so checked skills survive page refreshes and even full browser restarts.

In this step, get ready to:
  • Read saved progress from localStorage when the app loads.
  • Write progress to localStorage every time it changes.
  • Verify that progress persists across refreshes and browser restarts.
Read saved progress from localStorage

Right now, the progress state starts as an empty object every time the component mounts. You need to check localStorage first and use whatever is saved there.

  • In src/App.jsx, find the import line at the top of the file:
import { useState } from 'react';
  • Add useEffect to the import so the line looks like this:
import { useState, useEffect } from 'react';
  • Now find the progress state line inside the App function:
const [progress, setProgress] = useState({});
  • Replace that entire line with this function initializer:
const [progress, setProgress] = useState(() => {
  const saved = localStorage.getItem('pathfinder-progress');
  return saved ? JSON.parse(saved) : {};
});

What is a function initializer?

Passing a function to useState (instead of a plain value) tells React to run that function only once, when the component first mounts. This avoids reading localStorage on every single re-render.

If localStorage has a saved JSON string under the key pathfinder-progress, the initializer parses it back into an object. If nothing is saved yet, it falls back to an empty object {} so the app still works for first-time visitors.

Write progress to localStorage on every change

Reading from localStorage is only half the solution. You also need to save the latest progress every time the user checks or unchecks a skill.

  • In src/App.jsx, add this useEffect hook directly below the useState block you just edited (and above the handleToggleSkill function):
useEffect(() => {
  localStorage.setItem('pathfinder-progress', JSON.stringify(progress));
}, [progress]);

What does this useEffect do?

The useEffect hook runs a side effect whenever its dependencies change. The dependency array [progress] tells React to re-run this effect every time the progress object updates.

Each time a checkbox is toggled, React updates state, the component re-renders, and then this effect fires to serialize the new progress into localStorage. The data is now permanent.

  • Save src/App.jsx.

Seeing an error in the browser?

Make sure useEffect is imported on line 1. If you only added the hook without updating the import, React will throw a reference error.

Check that the useEffect call is inside the App function body, not outside it. React hooks must be called at the top level of a component.

help me debug a useEffect error in my App.jsx

Test the full persistence flow

Before you test, think about what you expect to happen. When you refresh the page after checking some boxes, do you think the checkboxes will still be checked this time?

  • Switch to your browser at http://localhost:5173.
  • Click any specialization card to enter its roadmap.
  • Check off 3-4 skill boxes.
  • Refresh the page (press Cmd+R on macOS or Ctrl+R on Windows).

You are back on the card selection screen (that is expected because selectedPath resets on refresh). Click the same specialization card again.

Your checked boxes are still checked. The progress survived the page refresh.

  • Now try a different specialization. Click Back to Specializations and choose a different card.
  • Check off a few skills in this second path.
  • Navigate back and re-enter the first specialization.

Both specializations remember their own checked skills independently. Each skill has a unique ID, so the single progress object tracks all of them without collisions.

  • For the ultimate test, close the browser tab entirely. Then reopen http://localhost:5173 and navigate into either roadmap. All progress is still there.

Why does this work after closing the tab?

localStorage persists data on disk, not in memory. Closing a tab or quitting the browser does not erase it. Only clearing browser data or calling localStorage.removeItem() deletes it.

Checkboxes still resetting on refresh?

Open DevTools (press F12) and go to the Application tab. Under Local Storage, check if a pathfinder-progress key exists. If it is missing, the useEffect is not firing.

Make sure the dependency array is [progress] and not an empty array []. An empty array means the effect only runs once on mount, so it never saves updates.

help me debug why localStorage is not saving my progress

✔️ Awesome, I've got everything!

Great. Double check you have saved src/App.jsx.

ⓧ I'd like to double check the full code

Scroll up and confirm your src/App.jsx has all three changes, in order:

  • The import line includes both useState and useEffect.
  • The progress state uses a function initializer that calls localStorage.getItem('pathfinder-progress').
  • A useEffect hook with [progress] dependency calls localStorage.setItem to save the latest state.

Everything else in the file (the handleToggleSkill function, the JSX return block) stays exactly the same as before.

Your app now delivers on its core promise. Users can track their learning journey and never lose a single checkbox. Next up, take on the Secret Mission and add a visual progress dashboard that shows completion percentages at a glance.

Secret mission

Add a Progress Dashboard

Your app tracks progress, but users have to click into each roadmap to see how far along they are. Can you add a visual progress bar to each specialization card on the landing page? You will compute derived data from state and render dynamic visual elements that update in real-time.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally, so there are no ongoing costs.

Resources you used:

  • The running Vite dev server in your terminal.
  • The pathfinder project folder on your Desktop (includes all source code and node_modules).
  • Browser localStorage data stored under the key pathfinder-progress.

Keep everything running

No action needed. Choose this if you are still actively building or want to keep experimenting with the app.

  • Your dev server stays running at http://localhost:5173. You can continue editing files and seeing changes instantly.
  • Your progress data remains saved in localStorage.
  • The pathfinder folder stays on your Desktop.

Pause - I'll come back to this later

Shut down the running dev server to free up your terminal and system resources. Your project files and browser data stay intact for next time.

  • Switch to the terminal where the dev server is running.
  • Press Ctrl+C to stop the server.
  • Your pathfinder folder and localStorage data remain untouched. To resume later, open the folder in VS Code, open a terminal, and run npm run dev.

Delete - I don't want to use this again

Remove all project files and stored data completely.

  • Switch to the terminal where the dev server is running and press Ctrl+C to stop it.
  • Delete the project folder by running this command (macOS/Linux):
rm -rf ~/Desktop/pathfinder

On Windows, run this instead:

rmdir /s /q %USERPROFILE%\Desktop\pathfinder
  • Clear your localStorage data by opening your browser console (Cmd+Option+J on macOS or Ctrl+Shift+J on Windows) and running:
localStorage.removeItem('pathfinder-progress')

That removes all project traces from your machine and browser.

Nice Work!

Nice Work!

Nice work! You just built a fully interactive Tech Career Pathfinder app with React, complete with persistent progress tracking that survives browser restarts.

You've learned how to:

  • Build a React application from scratch using Vite, creating reusable components that accept props and render dynamic content.
  • Implement state-based view switching with conditional rendering as a lightweight alternative to a routing library.
  • Use localStorage to persist user data across browser sessions so progress is never lost.
  • Secret Mission: Added a progress dashboard with computed completion percentages and visual progress bars that update in real-time.

Ready to quiz yourself?