Write your first components — TSX rules, props, conditional rendering, lists and keys, events, and composition with children.
Why: everything on a React screen is a component — a plain function that returns TSX. Capitalized names are components; lowercase names are HTML tags. Note: in a Next.js app this file just goes anywhere under app/ or components/.
// components/Welcome.tsx
// A component is just a function that returns TSX
export default function Welcome() {
const name = 'Ada'
const frameworks = 2 + 1
return (
<section>
{/* Braces embed any JavaScript expression */}
<h1>Hello, {name}!</h1>
<p>You know {frameworks} frameworks.</p>
</section>
)
}
// Use it like a tag anywhere else:
// <Welcome />Why: TSX looks like HTML but compiles to JavaScript, so a few rules differ. Learn these four once and the error messages disappear.
export default function Rules() {
return (
// 1. Return ONE root element — group siblings with a fragment <>…</>
<>
{/* 2. It's className, not class (class is a reserved word in JS) */}
<p className="intro">TSX is TypeScript, not HTML.</p>
{/* 3. Every tag must close — including <img />, <input />, <br /> */}
<img src="/photo.svg" alt="Logo" />
{/* 4. style takes an object with camelCased properties */}
<p style={{ fontSize: 14, marginTop: 8 }}>Inline styles are objects.</p>
</>
)
}Why: props are how a parent passes data down to a child. They are read-only — a component never modifies its own props. Note: type them with a type alias and destructure them in the signature.
// Describe the props once, as a type — the ? marks an optional one
type BadgeProps = {
label: string
count?: number
}
// Destructure in the signature; = 0 is the fallback when count is omitted
function Badge({ label, count = 0 }: BadgeProps) {
return (
<span>
{label}: {count}
</span>
)
}
export default function Header() {
return (
<header>
{/* Strings go in quotes, everything else goes in {braces} */}
<Badge label="Inbox" count={3} />
<Badge label="Drafts" /> {/* count falls back to 0 */}
</header>
)
}Why: components constantly show different UI for different data — signed in or not, empty or full, admin or member. Three patterns cover all of it: early return, ternary, and &&. Note: guard numbers with an explicit comparison (unread > 0), because 0 && … renders the 0 itself.
type User = { name: string; unread: number; isAdmin: boolean }
function Inbox({ user }: { user: User | null }) {
// 1. Early return — handle the missing case first
if (!user) return <p>Please sign in.</p>
return (
<div>
{/* 2. Ternary — choose between two outputs */}
<h2>{user.isAdmin ? 'Admin Console' : 'Your Inbox'}</h2>
{/* 3. && — render something or nothing */}
{user.unread > 0 && <p>{user.unread} unread messages</p>}
</div>
)
}
export default function Page() {
return <Inbox user={{ name: 'Ada', unread: 2, isAdmin: false }} />
}Why: rendering arrays is map() plus a key. The key tells React which item is which between renders, so it can reorder instead of rebuilding. Note: use a stable id from your data — the array index breaks reordering, inserts, and input state.
type Task = { id: number; title: string; done: boolean }
const tasks: Task[] = [
{ id: 1, title: 'Learn TSX', done: true },
{ id: 2, title: 'Master hooks', done: false },
{ id: 3, title: 'Ship an app', done: false },
]
export default function TaskList() {
return (
<ul>
{tasks.map((task) => (
// key must be stable and unique — use the data's id, never the index
<li key={task.id}>
{task.done ? '[x]' : '[ ]'} {task.title}
</li>
))}
</ul>
)
}Why: interactivity starts with event handlers. Pass the function itself — onClick={handleClick} — don't call it; onClick={handleClick()} runs immediately on render. Note: 'use client' marks this as a Client Component, required for event handlers in Next.js.
'use client' // Required for event handlers
export default function Buttons() {
function handleClick() {
console.log('clicked!')
}
// Typed events — TypeScript knows e.target.value exists
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
console.log(e.target.value)
}
return (
<div>
{/* Pass the function — don't call it */}
<button onClick={handleClick}>Log</button>
{/* Wrap in an arrow function when you need to pass arguments */}
<button onClick={() => console.log('saved at', Date.now())}>Save</button>
<input onChange={handleChange} placeholder="Type here" />
</div>
)
}Why: instead of components with dozens of props, build a frame and let callers fill it. The children prop is what you nest between the tags — this is the pattern behind every Card, Layout, and Modal you will ever write.
// The component owns the frame; the caller owns the content
function Card({ title, children }: { title: string; children: React.ReactNode }) {
return (
<section className="card">
<h3>{title}</h3>
{children}
</section>
)
}
export default function Dashboard() {
return (
<Card title="Revenue">
<p>$12,400 this month</p>
<button>Details</button>
</Card>
)
}