Skip to main content

Chapter 1: Setup & Your First Component

Time to get your hands dirty. We'll scaffold the TaskFlow project with Vite, write JSX, understand props, and build your first real component โ€” <TaskCard />.

๐Ÿ“Œ Prerequisites: You've read Chapter 0 and understand React's mental model โ€” declarative UI, components as functions, data flows down, state is immutable.


๐Ÿง  Conceptsโ€‹

1. Scaffolding with Vite + React 19 + TypeScriptโ€‹

Every React app needs a build tool โ€” something that takes your JSX, TypeScript, and modern JavaScript and turns it into files the browser can run. We'll use Vite (pronounced "veet" โ€” French for "fast").

Why Vite?

  • Instant dev server โ€” uses native ES modules, no bundling during development
  • Lightning-fast HMR โ€” Hot Module Replacement updates your browser in milliseconds
  • First-class TypeScript โ€” zero config
  • Tiny output โ€” optimized production builds with Rollup under the hood

Other options exist (Next.js, Remix, Parcel), but Vite is the best starting point for learning React because it stays out of your way. No server-side rendering magic, no file-system routing โ€” just React.

The Project Structureโ€‹

After scaffolding, here's what you get:

taskflow/
โ”œโ”€โ”€ index.html โ† entry point (Vite serves this)
โ”œโ”€โ”€ package.json โ† dependencies & scripts
โ”œโ”€โ”€ tsconfig.json โ† TypeScript config
โ”œโ”€โ”€ vite.config.ts โ† Vite config
โ”œโ”€โ”€ public/ โ† static assets (favicon, etc.)
โ””โ”€โ”€ src/
โ”œโ”€โ”€ main.tsx โ† app entry: renders <App /> into DOM
โ”œโ”€โ”€ App.tsx โ† root component
โ”œโ”€โ”€ App.css โ† root styles
โ””โ”€โ”€ vite-env.d.ts โ† Vite type declarations

index.html is the single HTML page. It has one <div id="root"> โ€” React takes over from there:

<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>

main.tsx is where React mounts your app to the DOM:

import { createRoot } from 'react-dom/client';
import App from './App';

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

createRoot is React 18+'s way of initializing โ€” it enables concurrent features like automatic batching and transitions.


2. JSX Deep Diveโ€‹

You saw in Chapter 0 that JSX compiles to React.createElement() calls. Now let's master the syntax.

JSX is Expressions, Not Statementsโ€‹

Inside {} you can use any JavaScript expression โ€” something that produces a value:

// โœ… Expressions โ€” these all produce values
{2 + 2} // โ†’ 4
{user.name} // โ†’ "Alice"
{isActive ? 'Yes' : 'No'} // โ†’ "Yes" or "No"
{items.length} // โ†’ 3
{formatDate(new Date())} // โ†’ "Feb 4, 2026"

You cannot use statements (things that don't produce values):

// โŒ Statements โ€” these won't work in JSX
{if (isActive) { return 'Yes' }} // SyntaxError!
{for (let i = 0; i < 5; i++) {}} // SyntaxError!
{let x = 5} // SyntaxError!

Conditional Renderingโ€‹

Three patterns, each for different situations:

Ternary โ€” when you have two alternatives:

// โœ… Recommended: ternary for either/or
{isLoggedIn ? <Dashboard /> : <LoginPage />}

Early return โ€” when a condition means "don't render at all":

const AdminPanel = ({ user }: { user: User }) => {
if (!user.isAdmin) return null; // bail out early
return <div>Secret admin stuff</div>;
}

Logical AND (&&) โ€” tempting but has a gotcha:

// โš ๏ธ Careful with &&
{count && <span>{count} items</span>}
// If count is 0, this renders "0" on screen! Not nothing.

// โœ… Better: explicit boolean check
{count > 0 ? <span>{count} items</span> : null}

๐Ÿ’ก Vercel Best Practice: Prefer ternary over && for conditional rendering. The && operator will render falsy values like 0 and "" as visible text. Ternary makes the "else" case explicit โ€” you decide what happens (usually null).

Rendering Listsโ€‹

Use .map() to turn an array of data into an array of elements:

const fruits = ['Apple', 'Banana', 'Cherry'];

const FruitList = () => {
return (
<ul>
{fruits.map((fruit) => (
<li key={fruit}>{fruit}</li>
))}
</ul>
);
}

Every list item needs a key prop. We'll dive into why next.

JSX Rules to Rememberโ€‹

  1. Return a single root element โ€” wrap siblings in <div> or <> (Fragment):

    // โŒ Two root elements
    return (
    <h1>Title</h1>
    <p>Body</p>
    );

    // โœ… Fragment wrapper (no extra DOM node)
    return (
    <>
    <h1>Title</h1>
    <p>Body</p>
    </>
    );
  2. Close all tags โ€” <img />, <input />, <br /> (self-closing required)

  3. CamelCase attributes โ€” className, onClick, htmlFor, tabIndex

  4. Style is an object โ€” style={{ color: 'red', fontSize: 16 }} (not a string)


3. Props: Passing Data to Componentsโ€‹

Props (short for "properties") are how parent components pass data to children. They're the function arguments of your component:

// Defining: what props does this component accept?
interface GreetingProps {
name: string;
excited?: boolean; // optional prop
}

const Greeting = ({ name, excited = false }: GreetingProps) => {
return <h1>Hello, {name}{excited ? '!!!' : '.'}</h1>;
}

// Using: parent passes the data
<Greeting name="Alice" excited />
<Greeting name="Bob" />

TypeScript makes props safe. Define an interface, and you get autocomplete and type errors. This is one of the biggest wins of TypeScript + React.

Props Are Read-Onlyโ€‹

A component must never modify its own props. They're like function arguments โ€” you receive them, you use them, you don't change them.

// โŒ NEVER do this
const BadComponent = (props: { name: string }) => {
props.name = 'hacked'; // This is wrong (and TypeScript will yell at you)
return <div>{props.name}</div>;
}

If a child needs to communicate back to the parent, the parent passes a callback function as a prop:

// Parent passes a function
<TaskCard task={task} onDelete={() => deleteTask(task.id)} />

// Child calls it
const TaskCard = ({ task, onDelete }: TaskCardProps) => {
return (
<div>
<span>{task.title}</span>
<button onClick={onDelete}>Delete</button>
</div>
);
}

Default Propsโ€‹

Use JavaScript default parameters โ€” no special React API needed:

const Button = ({ variant = 'primary', size = 'md' }: ButtonProps) => {
return <button className={`btn-${variant} btn-${size}`}>Click</button>;
}

4. The key Prop: Why Lists Need Itโ€‹

When React diffs a list of elements, it needs to match old elements with new ones. Without key, React can only compare by position โ€” which breaks horribly when items are reordered, added, or removed.

Without keys (by position):
Old: [Task A, Task B, Task C]
New: [Task B, Task C] โ† removed Task A

React thinks:
- Position 0: Task A โ†’ Task B (update text)
- Position 1: Task B โ†’ Task C (update text)
- Position 2: Task C โ†’ (remove)

It updated TWO elements and removed one. Should have just removed one!

With keys, React matches by identity:

With keys:
Old: [A(key=1), B(key=2), C(key=3)]
New: [B(key=2), C(key=3)]

React knows:
- key=1 is gone โ†’ remove it
- key=2 and key=3 are unchanged โ†’ do nothing

One removal. Correct and fast.

Rules for keys:

  • Must be unique among siblings (not globally)
  • Must be stable โ€” the same item always gets the same key
  • Never use array index as key if the list can reorder/filter
    • Index-as-key causes bugs with component state (inputs, animations)
  • Use IDs from your data โ€” task.id, user.email, etc.
// โŒ Bad โ€” index changes when items are reordered
{tasks.map((task, index) => (
<TaskCard key={index} task={task} />
))}

// โœ… Good โ€” stable identifier from data
{tasks.map((task) => (
<TaskCard key={task.id} task={task} />
))}

5. Your First Component: <TaskCard />โ€‹

Let's put it all together. A component is just a function that:

  1. Accepts props (typed with an interface)
  2. Returns JSX
interface Task {
id: string;
title: string;
completed: boolean;
}

interface TaskCardProps {
task: Task;
}

const TaskCard = ({ task }: TaskCardProps) => {
return (
<div className="task-card">
<span className={task.completed ? 'completed' : ''}>
{task.title}
</span>
</div>
);
}

This component is:

  • Pure โ€” same props always produce same output
  • Declarative โ€” describes what the UI looks like, not how to update it
  • Composable โ€” can be used inside any parent component
  • Typed โ€” TypeScript ensures you pass the right data

๐Ÿ†• React 19: ref as a Regular Propโ€‹

In React 18 and earlier, if you wanted to forward a ref to a DOM element inside your component, you had to wrap it in forwardRef:

// React 18 โ€” boilerplate!
const TaskCard = forwardRef<HTMLDivElement, TaskCardProps>(
const TaskCard = ({ task }, ref) => {
return <div ref={ref} className="task-card">...</div>;
}
);

In React 19, ref is just a prop. No wrapper needed:

// React 19 โ€” clean!
const TaskCard = ({ task, ref }: TaskCardProps & { ref?: React.Ref<HTMLDivElement> }) => {
return <div ref={ref} className="task-card">...</div>;
}

You don't need to understand refs yet (they let you directly access DOM elements), but know that React 19 killed a lot of boilerplate here. forwardRef will eventually be deprecated.


๐Ÿ’ก Examplesโ€‹

Example 1: JSX Expressionsโ€‹

const UserGreeting = ({ user }: { user: { name: string; age: number; isVIP: boolean } }) => {
return (
<div>
<h2>Welcome, {user.name}!</h2>
<p>Age: {user.age} ({user.age >= 18 ? 'Adult' : 'Minor'})</p>
{user.isVIP ? <span className="badge">โญ VIP</span> : null}
<p>Account created: {new Date().toLocaleDateString()}</p>
</div>
);
}

Example 2: Rendering a List with Keysโ€‹

interface Pokemon {
id: number;
name: string;
type: string;
}

const PokemonList = ({ pokemon }: { pokemon: Pokemon[] }) => {
if (pokemon.length === 0) {
return <p>No Pokรฉmon found. Touch grass.</p>;
}

return (
<ul>
{pokemon.map((p) => (
<li key={p.id}>
<strong>{p.name}</strong> โ€” {p.type}
</li>
))}
</ul>
);
}

Example 3: Component with Multiple Propsโ€‹

interface AlertProps {
message: string;
severity: 'info' | 'warning' | 'error';
dismissible?: boolean;
}

const Alert = ({ message, severity, dismissible = true }: AlertProps) => {
const icons = {
info: 'โ„น๏ธ',
warning: 'โš ๏ธ',
error: '๐Ÿšจ',
};

return (
<div className={`alert alert-${severity}`}>
<span>{icons[severity]}</span>
<p>{message}</p>
{dismissible ? <button>โœ•</button> : null}
</div>
);
}

Example 4: Fragments and Multiple Elementsโ€‹

const UserStats = ({ posts, followers }: { posts: number; followers: number }) => {
return (
<>
<dt>Posts</dt>
<dd>{posts.toLocaleString()}</dd>
<dt>Followers</dt>
<dd>{followers.toLocaleString()}</dd>
</>
);
}

// Usage โ€” Fragment lets you return multiple <dt>/<dd> without wrapper div
const Profile = () => {
return (
<dl>
<UserStats posts={142} followers={3800} />
</dl>
);
}

๐Ÿ”จ Project Task: Set Up TaskFlowโ€‹

Time to build! By the end of this section, you'll have a running TaskFlow app that displays a list of tasks.

Step 1: Create the Projectโ€‹

npm create vite@latest taskflow -- --template react-ts
cd taskflow
npm install

This gives you a React 19 + TypeScript project. Verify it works:

npm run dev

Open http://localhost:5173 โ€” you should see the Vite + React starter page.

Step 2: Clean Up the Scaffoldโ€‹

Delete the files you don't need:

rm src/App.css src/assets/react.svg

Replace src/App.tsx with a clean starting point:

const App = () => {
return (
<div className="app">
<h1>TaskFlow</h1>
<p>Your task management app.</p>
</div>
);
}

export default App;

Replace src/index.css with some minimal styles:

:root {
font-family: Inter, system-ui, -apple-system, sans-serif;
line-height: 1.6;
color: #213547;
background-color: #f8f9fa;
}

* {
margin: 0;
padding: 0;
box-sizing: border-box;
}

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

h1 {
font-size: 2rem;
margin-bottom: 0.5rem;
}

.task-card {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1rem;
background: white;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
margin-bottom: 0.5rem;
}

.task-card .completed {
text-decoration: line-through;
opacity: 0.6;
}

.task-list {
margin-top: 1.5rem;
}

Step 3: Define the Task Typeโ€‹

Create src/types.ts:

export interface Task {
id: string;
title: string;
completed: boolean;
createdAt: Date;
}

We'll use this type everywhere. Having it in a separate file avoids circular imports later.

Step 4: Create <TaskCard />โ€‹

Create src/components/TaskCard.tsx:

import type { Task } from '../types';

interface TaskCardProps {
task: Task;
}

const TaskCard = ({ task }: TaskCardProps) => {
return (
<div className="task-card">
<span className={task.completed ? 'completed' : ''}>
{task.title}
</span>
<small>
{task.createdAt.toLocaleDateString()}
</small>
</div>
);
}

export default TaskCard;

Step 5: Render a List of Tasksโ€‹

Update src/App.tsx:

import TaskCard from './components/TaskCard';
import type { Task } from './types';

// Hardcoded for now โ€” we'll add state in Chapter 2!
const SAMPLE_TASKS: Task[] = [
{
id: '1',
title: 'Learn React fundamentals',
completed: true,
createdAt: new Date('2026-01-15'),
},
{
id: '2',
title: 'Build TaskFlow app',
completed: false,
createdAt: new Date('2026-02-01'),
},
{
id: '3',
title: 'Master TypeScript generics',
completed: false,
createdAt: new Date('2026-02-03'),
},
];

const App = () => {
return (
<div className="app">
<h1>๐Ÿ“‹ TaskFlow</h1>
<p>{SAMPLE_TASKS.filter((t) => !t.completed).length} tasks remaining</p>

<div className="task-list">
{SAMPLE_TASKS.length > 0 ? (
SAMPLE_TASKS.map((task) => (
<TaskCard key={task.id} task={task} />
))
) : (
<p>No tasks yet. Add one!</p>
)}
</div>
</div>
);
}

export default App;

Notice:

  • Each <TaskCard> gets a key={task.id} โ€” stable, unique identifier
  • We use ternary for the empty-state check (not &&)
  • The count uses .filter() โ€” derived from data, not stored separately

Step 6: Verifyโ€‹

Run npm run dev and check your browser. You should see:

  • "๐Ÿ“‹ TaskFlow" heading
  • "2 tasks remaining" counter
  • Three task cards, one with strikethrough (completed)

๐ŸŽ‰ Your first React components are alive!


๐Ÿงช Challengeโ€‹

  1. Add more fields to Task โ€” add a priority field ('low' | 'medium' | 'high') and display a colored dot or emoji in <TaskCard /> based on priority.

  2. Build a <TaskStats /> component that receives the full task array and displays: total tasks, completed count, and completion percentage.

  3. Conditional CSS classes โ€” create a helper function cn(...classes: (string | false | undefined)[]) that joins class names, filtering out falsy values. Use it in TaskCard:

    <div className={cn('task-card', task.completed && 'faded', task.priority === 'high' && 'urgent')}>

๐Ÿ“š Further Readingโ€‹


Next up: Chapter 2 โ€” State & Events โ†’

Right now TaskFlow is static โ€” a snapshot frozen in time. In the next chapter, we'll add state and event handling to make tasks addable, deletable, and completable.