top of page

Building Your First React App in VSCode — With a Little Help From Copilot

  • Writer: MacSmithAI
    MacSmithAI
  • Apr 17
  • 6 min read

If you've been following this series you already have VSCode set up with GitHub Copilot agent mode. This post puts that setup to work on something concrete: a small React app, built on a Mac, from an empty directory to a working dev server in about 15 minutes.

The goal isn't to teach React from scratch — there are much better resources for that. It's to show the end-to-end workflow of building a modern React app with AI assistance sitting right next to the editor. If you've been reading and not building, this is the "now do the thing" post.


One quick note on tooling

The React team officially deprecated create-react-app in February 2025. If you're following an old tutorial that starts with npx create-react-app, close that tab. The current defaults are Vite for standalone apps (what we'll use here) or Next.js for full-stack apps with routing and server components.


Vite starts a dev server in under two seconds, has hot module replacement that feels instant, and doesn't bury you in configuration. For a first React app, it's the right tool.


What you need

  • Node.js 20 or later. Check with node --version. If you don't have it, brew install node.

  • VSCode with GitHub Copilot enabled. Agent mode is a nice-to-have, not a requirement — the setup works without it.

  • About 15 minutes and a directory to work in.


Step 1: Scaffold the project

Open Terminal (or VSCode's integrated terminal with ⌃`) and run:

npm create vite@latest my-todo-app

Vite's CLI walks you through a few prompts. Pick:

  • Framework: React

  • Variant: JavaScript (or TypeScript if you're comfortable with it — the rest of this post uses JavaScript)

Then:

cd my-todo-app
npm install
npm run dev

The dev server starts on http://localhost:5173. Open it in a browser — you should see the Vite + React welcome page with a counter button. That means everything works.

Open the project in VSCode:

code .

If code isn't on your PATH, open VSCode manually and drag the folder in. Either way, keep the dev server running in the terminal — it watches for changes and hot-reloads the browser as you save.


Step 2: Understand what you're looking at

Vite's React template gives you a minimal project structure. The files that matter:

  • index.html — the HTML entry point. Vite loads your React code into the <div id="root"> here.

  • src/main.jsx — the JavaScript entry point. Mounts your root component to that div.

  • src/App.jsx — the root component. This is the file you'll spend most of your time in.

  • src/App.css and src/index.css — styles.

  • package.json — dependencies and scripts (dev, build, preview).

Ignore node_modules/ — that's just your installed packages, you never edit it directly. Ignore public/ for now — it's for static assets like favicons.


Step 3: Replace App.jsx with a todo list

Open src/App.jsx and replace its contents with this:

import { useState } from 'react'
import './App.css'

function App() {
  const [todos, setTodos] = useState([])
  const [input, setInput] = useState('')

  const addTodo = () => {
    if (input.trim() === '') return
    setTodos([...todos, { id: Date.now(), text: input, done: false }])
    setInput('')
  }

  const toggleTodo = (id) => {
    setTodos(todos.map(t =>
      t.id === id ? { ...t, done: !t.done } : t
    ))
  }

  const deleteTodo = (id) => {
    setTodos(todos.filter(t => t.id !== id))
  }

  return (
    <div className="app">
      <h1>Things to do</h1>
      <div className="input-row">
        <input
          type="text"
          value={input}
          onChange={(e) => setInput(e.target.value)}
          onKeyDown={(e) => e.key === 'Enter' && addTodo()}
          placeholder="What needs doing?"
        />
        <button onClick={addTodo}>Add</button>
      </div>
      <ul>
        {todos.map(todo => (
          <li key={todo.id} className={todo.done ? 'done' : ''}>
            <span onClick={() => toggleTodo(todo.id)}>{todo.text}</span>
            <button onClick={() => deleteTodo(todo.id)}>×</button>
          </li>
        ))}
      </ul>
    </div>
  )
}

export default App

Save the file. The browser should refresh automatically and show your todo app — header, input box, add button, empty list. Type something, press Enter, and it appears. Click a todo to toggle it done; click the × to remove it.

What's happening in this file, in plain English: useState gives you two pieces of state — the list of todos and the current input text. Three functions (addTodo, toggleTodo, deleteTodo) update that state. The return block is JSX — HTML-like syntax that describes what the component should render. When state changes, React re-renders automatically.


Step 4: Make it look like something

The default Vite styling is fine for a demo and ugly for anything else. Replace src/App.css with:

.app {
  max-width: 480px;
  margin: 3rem auto;
  padding: 2rem;
  font-family: -apple-system, sans-serif;
}

h1 {
  font-size: 1.5rem;
  margin-bottom: 1.5rem;
}

.input-row {
  display: flex;
  gap: 0.5rem;
  margin-bottom: 1.5rem;
}

input {
  flex: 1;
  padding: 0.6rem 0.8rem;
  font-size: 1rem;
  border: 1px solid #ccc;
  border-radius: 6px;
}

button {
  padding: 0.6rem 1rem;
  font-size: 1rem;
  background: #0066cc;
  color: white;
  border: none;
  border-radius: 6px;
  cursor: pointer;
}

button:hover {
  background: #0052a3;
}

ul {
  list-style: none;
  padding: 0;
}

li {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0.6rem 0.8rem;
  border-bottom: 1px solid #eee;
}

li span {
  cursor: pointer;
  flex: 1;
}

li.done span {
  text-decoration: line-through;
  color: #999;
}

li button {
  background: transparent;
  color: #999;
  font-size: 1.2rem;
  padding: 0 0.5rem;
}

li button:hover {
  background: transparent;
  color: #c00;
}

Save. The app refreshes. Now it looks like something you wouldn't be embarrassed to show someone.


Step 5: Let Copilot extend it

This is where the VSCode + Copilot setup from earlier in the series pays off. The app works but it has an obvious flaw: refresh the page and your todos are gone. Let's fix that with Copilot agent mode instead of writing the code ourselves.

Open the Copilot Chat pane (⌃⌘I), switch the mode dropdown to Agent, and prompt:

In src/App.jsx, make the todos persist to localStorage so they survive a page refresh. Load them on mount and save them whenever the list changes. Don't change the UI or any other behavior.

Copilot will read the file, propose an edit, and show you a diff. It'll add a useEffect to save changes and modify the initial state to read from localStorage. Review the diff, accept it, and refresh the browser. Add a todo, refresh again. It's still there.

Try a couple more small extensions to get a feel for the loop:

Add a counter above the list that shows how many todos are left to do (not done).
Add a "Clear completed" button that removes all done todos at once. Only show it when there are completed todos to clear.

The agent-mode pattern is straightforward: describe what you want, let it make the change, read the diff before accepting. You learn React faster reading AI-generated code and asking "why did it do it this way?" than you would writing every line yourself. The learning happens in the diffs.


Step 6: Build it for production

When you're ready to deploy (or just want to see what the production build looks like), stop the dev server (⌃C) and run:

npm run build
npm run preview

The first command bundles your app into dist/ — minified, tree-shaken, ready to deploy. The second serves that bundle locally so you can verify it works. The contents of dist/ are static files — you can drop them on any web host (Netlify, Vercel, Cloudflare Pages, S3, GitHub Pages, a server you own) and they'll work.


Things I'd extend next

A todo list is a toy. The things that turn it into something you'd actually use are mostly obvious extensions, and each one is a good Copilot prompt rather than a rabbit hole:

  • Categories or tags for todos, with filtering

  • Due dates with visual urgency indicators

  • Keyboard shortcuts for common actions

  • Drag-to-reorder with @dnd-kit

  • A dark mode toggle that respects prefers-color-scheme

  • Sync to a backend so it works across devices

The last one is where you graduate from Vite to Next.js or React Router — anything with real data fetching or routing benefits from a framework rather than a bare Vite app. But for local-only apps and small tools, Vite is the right choice and there's no reason to add complexity you don't need.


A few VSCode extensions worth installing

If you're going to keep writing React, these earn their keep quickly:

  • ES7+ React/Redux/React-Native snippets — type rfc and get a full functional component scaffold

  • Prettier — auto-formats your code on save, so you stop arguing with yourself about indentation

  • Error Lens — shows errors and warnings inline next to the offending line instead of requiring you to hover

  • Path Intellisense — autocomplete for import paths

Don't install everything at once — add one, use it for a few days, then add the next. Extension bloat is a real drag on VSCode performance and it's easy to end up with twelve things you forgot you installed.

Comments


bottom of page