What You'll Build
You will build a small, reusable component library that exercises everything from Module 1: a Button with variants, a controlled Input, a Card for composition, a Toggle with internal state, and a Modal that demonstrates conditional rendering and events. Each component is configured by props, manages its own state where appropriate, and handles events cleanly — a practical foundation you will reuse throughout the course.
The goal is to think in components: small, focused, prop-driven pieces that compose into richer UI. You will practise the props-versus-state decision, controlled inputs, event handlers that update state or call callbacks, and composition via children — the core skills every later module assumes.
Prerequisites
- Completion of lessons 01–04, or equivalent familiarity with components, JSX, props, state, and events.
- Node.js 18+ and a React 19 project scaffolded with Vite (npm create vite@latest -- --template react).
- Comfort running a dev server (npm run dev) and editing JSX files.
- Basic CSS for light styling (or use plain class names you style later).
- Understanding of useState and one-way data flow from this module.
Setup & Project Structure
Scaffold a React project with Vite and create a components folder to hold each reusable piece. You will build Button, Input, Card, Toggle, and Modal as separate files, then compose them in App to see them work together. Keep each component focused and prop-driven.
Decide up front what is props versus state for each: Button and Card are stateless (configured purely by props and children); Input is controlled by its parent (value + onChange props); Toggle and Modal own small pieces of state. This planning is the core skill the exercise reinforces.
# Scaffold and run a React 19 + Vite project
npm create vite@latest component-lib -- --template react
cd component-lib && npm install && npm run dev
# Structure
# src/
# ├── components/
# │ ├── Button.jsx # stateless, prop-driven (variant, onClick, children)
# │ ├── Input.jsx # controlled (value, onChange props)
# │ ├── Card.jsx # composition via children
# │ ├── Toggle.jsx # owns boolean state
# │ └── Modal.jsx # conditional render + events
# └── App.jsx # composes them allStep 1 — Stateless, Prop-Driven Button and Card
Build Button as a pure, stateless component configured entirely by props: a variant prop selects styling, children provides the label, and onClick passes through to the underlying button. Build Card similarly, using children to compose arbitrary content inside a styled container — demonstrating composition.
Neither component owns state; they are pure functions of their props, illustrating that many components need no state at all. The variant prop maps to a CSS class, and spreading remaining props (...rest) onto the element keeps the component flexible without enumerating every attribute.
// Button.jsx — stateless, configured by props, composes via children
export function Button({ variant = "primary", children, ...rest }) {
return (
<button className={`btn btn-${variant}`} {...rest}>
{children}
</button>
);
}
// Card.jsx — composition: render whatever children are passed inside
export function Card({ title, children }) {
return (
<div className="card">
{title && <h3 className="card-title">{title}</h3>}
<div className="card-body">{children}</div>
</div>
);
}Step 2 — Controlled Input and Stateful Toggle
Build Input as a controlled component: it takes value and onChange props, so the parent owns the field's state and the input always reflects it. This is the canonical event-to-state loop — the input renders from value and reports changes via onChange — making the parent the single source of truth.
Build Toggle as a component that owns its own boolean state with useState, flipping it on click and optionally notifying a parent via an onChange callback. Contrast the two: Input is controlled by the parent (state lives up), while Toggle manages its own state (state lives in) — the props-versus-state decision made concrete.
// Input.jsx — CONTROLLED: parent owns the value via props
export function Input({ value, onChange, ...rest }) {
return (
<input
className="input"
value={value}
onChange={(e) => onChange(e.target.value)} // report change up
{...rest}
/>
);
}
// Toggle.jsx — owns its own state, optionally notifies parent
import { useState } from "react";
export function Toggle({ defaultOn = false, onChange }) {
const [on, setOn] = useState(defaultOn);
function handle() {
const next = !on;
setOn(next);
onChange?.(next); // optional callback up
}
return <button aria-pressed={on} onClick={handle}>{on ? "On" : "Off"}</button>;
}Step 3 — Modal with Conditional Rendering and Events
Build Modal to demonstrate conditional rendering and event handling: it takes an open boolean and an onClose callback, renders nothing when closed, and renders an overlay plus content when open. Clicking the overlay or a close button calls onClose, and clicking the inner content uses stopPropagation so it does not close.
The Modal does not own its open state — the parent does — keeping it controlled and reusable. This shows conditional rendering (return null when closed), event handling (onClose callbacks), and stopPropagation to prevent the overlay click from firing when the content is clicked, all patterns from the events lesson.
// Modal.jsx — controlled by parent; conditional render + event handling
export function Modal({ open, onClose, title, children }) {
if (!open) return null; // conditional render
return (
<div className="overlay" onClick={onClose}> {/* click backdrop to close */}
<div className="modal" onClick={(e) => e.stopPropagation()}> {/* don't close */}
<header>
<h3>{title}</h3>
<Button variant="ghost" onClick={onClose}>×</Button>
</header>
<div>{children}</div>
</div>
</div>
);
}Step 4 — Testing & Verification
Compose all five components in App and verify each behaviour: Buttons render with variants and fire onClick; the controlled Input updates parent state on every keystroke; the Toggle flips and reports its state; and the Modal opens, closes via backdrop and button, and does not close when its content is clicked. Confirm that stateless components re-render purely from props and stateful ones manage their own state independently.
// App.jsx — compose and exercise the library
import { useState } from "react";
import { Button } from "./components/Button";
import { Input } from "./components/Input";
import { Card } from "./components/Card";
import { Toggle } from "./components/Toggle";
import { Modal } from "./components/Modal";
export default function App() {
const [name, setName] = useState(""); // parent owns Input's value
const [open, setOpen] = useState(false); // parent owns Modal's open state
return (
<Card title="Component Library">
<Input value={name} onChange={setName} placeholder="Your name" />
<Toggle defaultOn onChange={(on) => console.log("toggle:", on)} />
<Button onClick={() => setOpen(true)}>Open Modal</Button>
<Modal open={open} onClose={() => setOpen(false)} title={`Hi ${name || "there"}`}>
<p>Composed from reusable components.</p>
</Modal>
</Card>
);
}Warning: Keep controlled components truly controlled — if Input takes a value prop, always pair it with onChange so the parent can update that value; otherwise the field appears frozen because state never changes. Mixing a value prop with no onChange (or switching between controlled and uncontrolled) produces React warnings and confusing input behaviour. Decide controlled or uncontrolled and stay consistent.
Extension Challenge: Add a controlled-vs-uncontrolled variant to Input by allowing it to manage its own state internally when no value prop is given (falling back to useState), making it work both ways. Then add keyboard handling to Modal — close it on the Escape key using an effect (previewing the next module) — and a Button loading state that disables the button and shows a spinner while an async onClick runs.
- Build small, focused, reusable components configured by props; compose them via children.
- Stateless components (Button, Card) are pure functions of props — many components need no state.
- Controlled components (Input) take value + onChange so the parent owns the state (single source of truth).
- Stateful components (Toggle) own state with useState and optionally report changes via a callback.
- Conditional rendering (return null when closed) and event handling (onClose, stopPropagation) drive the Modal.
- Decide props-versus-state deliberately per component, and keep controlled inputs consistently controlled.