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 like0and""as visible text. Ternary makes the "else" case explicit โ you decide what happens (usuallynull).
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โ
-
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>
</>
); -
Close all tags โ
<img />,<input />,<br />(self-closing required) -
CamelCase attributes โ
className,onClick,htmlFor,tabIndex -
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:
- Accepts props (typed with an interface)
- 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:
refas a Regular PropโIn React 18 and earlier, if you wanted to forward a
refto a DOM element inside your component, you had to wrap it inforwardRef:// React 18 โ boilerplate!
const TaskCard = forwardRef<HTMLDivElement, TaskCardProps>(
const TaskCard = ({ task }, ref) => {
return <div ref={ref} className="task-card">...</div>;
}
);In React 19,
refis 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.
forwardRefwill 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 akey={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โ
-
Add more fields to
Taskโ add apriorityfield ('low' | 'medium' | 'high') and display a colored dot or emoji in<TaskCard />based on priority. -
Build a
<TaskStats />component that receives the full task array and displays: total tasks, completed count, and completion percentage. -
Conditional CSS classes โ create a helper function
cn(...classes: (string | false | undefined)[])that joins class names, filtering out falsy values. Use it inTaskCard:<div className={cn('task-card', task.completed && 'faded', task.priority === 'high' && 'urgent')}>
๐ Further Readingโ
- React docs: Your First Component โ official guide to components and JSX
- React docs: Writing Markup with JSX โ JSX rules and gotchas
- React docs: Rendering Lists โ everything about
map(),key, and list rendering - React docs: Passing Props to a Component โ props deep dive
- Vite docs: Getting Started โ understanding your build tool
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.