Week 1: React Fundamentals, JSX & the Component Model

Every pattern later in this course — hooks in Week 2, routing in Week 6, server state in Week 8 — is built on top of one idea: a component is a function that takes data in and returns UI out. This week makes that idea concrete: what JSX actually compiles to, how React decides what to re-render, and the props-vs-state distinction that trips up almost everyone the first time they meet it.

Module 1 of 17 Week 1 of 28 ~3–4 Hours Hands-on Exercise Included

By the end of this week, you'll be able to

  • Explain what JSX compiles to and why React re-renders when it does
  • Build components that split cleanly along props vs. state
  • Set up and navigate a Vite + React + TypeScript project

1. Project Setup with Vite

This course uses Vite to scaffold and run React apps — it's faster than older tooling like Create React App (now discontinued) and is what most new React projects reach for today. One command gets you a working TypeScript project:

terminal
npm create vite@latest week-01 -- --template react-ts
cd week-01
npm install
npm run dev

npm run dev starts a local dev server (usually at http://localhost:5173) with hot module replacement — save a file and the browser updates instantly, without a full page reload or losing component state.

project layout
week-01/
├── index.html
├── package.json
├── vite.config.ts
└── src/
    ├── main.tsx      ← mounts the app into index.html
    ├── App.tsx        ← root component
    └── App.css
src/main.tsx
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';

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

createRoot(...).render(<App />) is the one line where React takes over a DOM node and starts managing everything inside it. Everything else in this course happens inside that tree.

2. JSX & Elements

JSX — the HTML-like syntax inside your .tsx files — isn't valid JavaScript on its own. A build tool (Vite, via esbuild) compiles it into plain function calls before your code ever runs in the browser.

what you write
const heading = <h1 className="title">Hello, React</h1>;
what it compiles to
const heading = jsx('h1', { className: 'title', children: 'Hello, React' });

That function call returns a plain JavaScript object — a React element — describing what should appear on screen: a type, some props, and children. React elements are cheap, immutable descriptions, not real DOM nodes yet.

expressions in JSX
function Greeting({ name }: { name: string }) {
  const hour = new Date().getHours();
  const greeting = hour < 12 ? 'Good morning' : 'Good afternoon';

  return (
    <div>
      <p>{greeting}, {name}!</p>
      {name === 'Ada' && <span>⭐ Welcome back</span>}
    </div>
  );
}

Curly braces {'{ }'} drop back into JavaScript inside JSX — any expression works, including ternaries for either/or rendering and && for conditionally rendering something or nothing.

Why className, not class?

JSX attributes are JavaScript object properties under the hood, and class is a reserved word in JavaScript. This is also why event handlers are onClick (camelCase), not onclick — JSX props follow JS naming conventions, not HTML's.

3. The Virtual DOM: Why React Re-Renders

Every time a component's state or props change, React calls that component's function again, producing a new tree of elements. Instead of touching the real DOM immediately, React diffs the new element tree against the previous one, and only applies the minimal set of real DOM changes needed.

Counter.tsx
import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  console.log('Counter rendered'); // logs on every click

  return (
    <button onClick={() => setCount(count + 1)}>
      Clicked {count} times
    </button>
  );
}

Clicking the button calls setCount, which schedules a re-render: Counter() runs again from the top, producing a new element tree. React compares it to the previous tree, sees only the text node changed, and updates just that text node in the real DOM — not the whole button, and definitely not the whole page.

This is the core mental model you'll build on for the rest of the course: a render is not the same thing as a DOM update. "Rendering" means React calling your component function to figure out what should be on screen; the actual DOM only changes where the result differs from last time.

4. Props vs. State — and One-Way Data Flow

This is the distinction that causes the most confusion in your first week with React, so it's worth being precise: props are data passed into a component by its parent — read-only from the component's own point of view. State is data a component owns and can change itself, via useState (which you'll go deep on in Week 2).

props-vs-state.tsx
interface UserCardProps {
  name: string;   // prop -- the parent decides this, UserCard can't change it
}

function UserCard({ name }: UserCardProps) {
  const [expanded, setExpanded] = useState(false); // state -- UserCard owns this

  return (
    <div>
      <p>{name}</p>
      <button onClick={() => setExpanded(!expanded)}>
        {expanded ? 'Show less' : 'Show more'}
      </button>
      {expanded && <p>Extra details about {name}...</p>}
    </div>
  );
}

// The parent controls the prop; UserCard controls its own state
<UserCard name="Ada Lovelace" />

Data flows one way: from parent to child, via props. A child can never directly hand a new value back up into its parent's state — if a child needs to change something the parent owns, the parent passes down a function as a prop, and the child calls it:

lifting state up
function Parent() {
  const [selected, setSelected] = useState<string | null>(null);

  return <OptionList selected={selected} onSelect={setSelected} />;
}

function OptionList({ selected, onSelect }: {
  selected: string | null;
  onSelect: (value: string) => void;
}) {
  return (
    <button onClick={() => onSelect('blue')}>
      {selected === 'blue' ? '✓ ' : ''}Blue
    </button>
  );
}

This pattern — called lifting state up — is how two sibling components end up "sharing" state: the state actually lives in their closest common parent, and both children receive it (and a way to change it) via props.

Why this matters later

Lifting state up works fine for a few components. Once you're lifting the same piece of state through five layers just to pass it down, that's the exact pain point Context (Week 10) and global state libraries solve.

5. Composing Components

A React app is a tree of components rendering other components. The children prop is what lets a component wrap arbitrary content it doesn't need to know the shape of — the same pattern behind every layout, card, and modal you'll build:

Card.tsx
interface CardProps {
  title: string;
  children: React.ReactNode;
}

function Card({ title, children }: CardProps) {
  return (
    <div className="card">
      <h3>{title}</h3>
      <div className="card__body">{children}</div>
    </div>
  );
}

// Usage -- Card has no idea what's inside it, and doesn't need to
<Card title="Profile">
  <UserCard name="Grace Hopper" />
  <button>Edit</button>
</Card>

Notice Card is completely reusable — it knows how to render a titled box, and nothing about what goes inside one. This "wrap arbitrary children" shape reappears constantly: layout components, modals, list wrappers, and — in Week 12 — the compound-component pattern built entirely on top of it.

6. Hands-on Exercise

Hands-on

Build a filterable, selectable profile directory from scratch

Practice the props/state/composition split by building a small, real UI in two parts — no external libraries, just what this week covered.

starter data
const people = [
  { id: 1, name: 'Ada Lovelace', role: 'Mathematician', bio: 'Wrote the first published algorithm.' },
  { id: 2, name: 'Grace Hopper', role: 'Computer Scientist', bio: 'Pioneered the compiler and COBOL.' },
  { id: 3, name: 'Alan Turing', role: 'Mathematician', bio: 'Formalized computation with the Turing machine.' },
  { id: 4, name: 'Katherine Johnson', role: 'Physicist', bio: 'Calculated the trajectories for NASA’s early crewed flights.' },
];

Part 1: Filterable list

Requirements:

  1. Scaffold a new Vite + React + TypeScript project and get it running with npm run dev.
  2. Build a PersonCard component that takes a person as props and renders their name and role.
  3. Build a PersonList component that owns a filterText piece of state (via useState) and a text input bound to it, rendering one PersonCard per person whose name matches the filter (case-insensitive).
  4. Wrap the whole list in a Card component (from Section 5) that takes a title prop and renders PersonList as its children.
  5. Add a "no results" message, rendered conditionally, when the filter matches nobody.
Hint

The filter state belongs in PersonList, not PersonCardPersonCard should stay a "dumb" component that only receives props. If you find yourself wanting state inside PersonCard for this exercise, that's a sign the state lives one level too low.

Part 2: Lift selection state to power a detail panel

Part 1 only exercised filtering. This part puts Section 4's "lifting state up" and Section 5's composition to work on the same app, proving that two components that look unrelated can share state through a common parent.

Requirements:

  1. Move the app's root component (call it App) up one level above Card/PersonList, and give App a new selectedId piece of state, typed number | null, starting at null.
  2. Thread an onSelect: (id: number) => void callback down through PersonList to each PersonCard — clicking anywhere on a card calls onSelect(person.id). PersonList should not own this state itself, only pass it through.
  3. Build a DetailPanel component that takes person: Person | null as a prop: render "Select a person to see their details" when null, otherwise the person's name, role, and bio.
  4. Render DetailPanel as a sibling of Card inside App, both driven by the same selectedId state — this is the "lifting state up" pattern from Section 4 applied for real, not just read about.
  5. Visually distinguish the selected PersonCard (a CSS class or inline style toggled by comparing person.id === selectedId).
  6. Stretch: extract the clickable chrome around each card's contents into a small Selectable wrapper component that takes children and an isSelected/onClick prop — decoupling "how a selectable box looks" from "what's inside it," the same shape as Card.
App.tsx — shape to fill in
function App() {
  const [selectedId, setSelectedId] = useState<number | null>(null);
  const selectedPerson = people.find(p => p.id === selectedId) ?? null;

  return (
    <div className="layout">
      <Card title="Team Directory">
        <PersonList
          people={people}
          selectedId={selectedId}
          onSelect={setSelectedId}
        />
      </Card>
      <DetailPanel person={selectedPerson} />
    </div>
  );
}
Hint

If passing selectedId/onSelect through PersonList just to reach PersonCard feels like unnecessary plumbing for a two-level tree, you're right — it is, a little. It's genuinely the correct pattern here. It only becomes a real problem once you're doing it through five or six layers, which is exactly the scenario Context (Week 10) exists to fix. Feeling the friction now is the point.

7. Knowledge Check

Four quick questions. Expand each to check your answer.

Q1

What does a line of JSX like <h1>Hi</h1> actually become before it reaches the browser?

The build tool compiles it into a plain function call (like jsx('h1', { children: 'Hi' })) that returns a JavaScript object describing the element — type, props, and children. It's a description of what should be on screen, not a real DOM node.

Q2

When state changes and a component "re-renders," does React necessarily touch the real DOM?

Not necessarily, and even when it does, not everywhere. "Re-rendering" means React calls the component function again and gets a new element tree, which it diffs against the previous tree. Only the parts of the real DOM that actually differ get updated — a render and a DOM update are two different things.

Q3

Why can't a child component directly modify a piece of state that lives in its parent?

React data flow is one-directional: props only travel from parent to child, never back up automatically. A child can only affect a parent's state if the parent explicitly passes down a function as a prop (like onSelect) for the child to call — the parent still owns and controls the actual state update.

Q4

What makes the children prop different from a regular prop like title?

Nothing structurally — it's still just a prop. The difference is convention and syntax: whatever you nest between a component's opening and closing JSX tags gets passed automatically as its children prop, which lets a wrapper component (like Card) render arbitrary, unknown content without needing a dedicated prop for every possible thing you might put inside it.