React is an open-source JavaScript library created by Meta (Facebook) for building user interfaces. It's the most popular UI library in the world, powering apps like Instagram, Netflix, Airbnb, and thousands more.
Why React?
function Welcome() {
return (
<div>
<h1>Hello, Caterpillar!</h1>
<p>Welcome to the world of React.</p>
</div>
)
}
// This component can now be used anywhere:
// <Welcome />Welcome, not welcome) — this is how React tells them apart from regular HTML tags.JSX is a syntax extension that lets you write HTML-like code inside JavaScript. It looks like HTML but has the full power of JavaScript behind it.
function Greeting() {
const name = "Caterpillar"
const itemCount = 42
return (
<div>
{/* Use {} to embed any JavaScript expression */}
<h1>Welcome to {name}</h1>
<p>We have {itemCount} machines in stock</p>
<p>That's {itemCount > 20 ? "a lot" : "a few"}!</p>
</div>
)
}JSX rules to remember:
{ } to embed any JavaScript expressionclassName instead of class to set CSS classes (since class is a reserved word in JS)key prop<> fragments to wrap multiple elements)Components are functions that return JSX. Props are how you pass data from a parent component to a child — think of them like function arguments.
// Define a component that accepts props
function ProductCard({ name, price, image }: {
name: string
price: number
image: string
}) {
return (
<div className="border rounded-lg p-4">
<img src={image} alt={name} />
<h2>{name}</h2>
<p>${price.toLocaleString()}</p>
</div>
)
}
// Use it — parent passes the data
function Catalog() {
return (
<div>
<ProductCard
name="Cat Heavy-Duty Air Filter"
price={29}
image="/product-1.jpg"
/>
<ProductCard
name="Cat HYDO Advanced Hydraulic Oil"
price={99}
image="/product-2.jpg"
/>
</div>
)
}State is data that can change over time. When state changes, React automatically re-renders the component to reflect the new data. The useState hook is how you add state to a component.
import { useState } from 'react'
function Counter() {
// useState returns [currentValue, setterFunction]
const [count, setCount] = useState(0)
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
<button onClick={() => setCount(0)}>
Reset
</button>
</div>
)
}Rules of useState:
useState at the top level of your component (never inside loops or conditions)setState({ ...state, key: newValue })count = count + 1 because it won't trigger a re-renderSide effects are anything that interacts with the outside world: fetching data, setting up subscriptions, updating the document title, or using browser APIs. The useEffect hook lets you run these after React renders your component.
import { useState, useEffect } from 'react'
function ProductListings() {
const [products, setProducts] = useState([])
const [loading, setLoading] = useState(true)
useEffect(() => {
// Runs once after the first render
fetch('/api/products')
.then(res => res.json())
.then(data => {
setProducts(data)
setLoading(false)
})
}, []) // Empty array = run once on mount
if (loading) return <p>Loading...</p>
return (
<ul>
{products.map(product => (
<li key={product.id}>{product.name}</li>
))}
</ul>
)
}Props are great for direct parent-to-child communication. But what happens when a deeply nested component needs data from way up the tree? You end up passing the same props through every component in between — even ones that don't use them. That's prop drilling.
function App() {
const [theme, setTheme] = useState<'light' | 'dark'>('light')
return <Layout theme={theme} setTheme={setTheme} />
}
// Layout doesn't use theme, just passes it through
function Layout({ theme, setTheme }) {
return (
<div>
<Header theme={theme} setTheme={setTheme} />
<Main theme={theme} />
</div>
)
}
// Header doesn't use theme either, just passes it
function Header({ theme, setTheme }) {
return <Toolbar theme={theme} setTheme={setTheme} />
}
// Toolbar finally uses it
function Toolbar({ theme, setTheme }) {
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Theme: {theme}
</button>
)
}Why this hurts:
Context.Context lets you put a value at the top of your component tree and read it from any descendant — no matter how deep — without passing props through the layers in between. Think of it as a "broadcast" that any subscribed component can tune into.
Context has three pieces:
createContext(defaultValue) — defines the shape of the broadcast<MyContext value={...}> — wraps the tree where the value should be availableuse(MyContext) — reads the current value from any descendant componentimport { createContext, use, useState } from 'react'
type Theme = 'light' | 'dark'
type ThemeContextValue = {
theme: Theme
setTheme: (t: Theme) => void
}
const ThemeContext = createContext<ThemeContextValue | null>(null)
function App() {
const [theme, setTheme] = useState<Theme>('light')
// Provide the value once, at the top
return (
<ThemeContext value={{ theme, setTheme }}>
<Layout />
</ThemeContext>
)
}
// Middle components no longer touch theme
function Layout() {
return (
<div>
<Header />
<Main />
</div>
)
}
function Header() {
return <Toolbar />
}
// Read it directly, no matter how deep
function Toolbar() {
const { theme, setTheme } = use(ThemeContext)!
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Theme: {theme}
</button>
)
}Context is powerful, but it's not a replacement for props. Use it for values that are truly global within a section of your app — things many components need but nobody wants to pass around manually.
| Good Fit for Context | Stick with Props |
|---|---|
| Theme (light / dark) | A single component's title or label |
| Current user / auth session | Data only used by direct children |
| Locale / translations | Frequently changing values that re-render lots of subscribers |
| Currently selected item in a list/grid | One-off configuration passed once |
'use client'). Server Components don't re-render, so they don't need it. We'll explore the Server/Client split in the next module.The key skill in React is learning to see any UI as a tree of components. Start with a design, draw boxes around each distinct piece, and that's your component structure.
// Step 1: Identify the pieces
// ┌─────────────────────────────┐
// │ ProductImage │
// │ ┌───────────────────────┐ │
// │ │ │ │
// │ └───────────────────────┘ │
// │ CategoryBadge │
// │ ProductTitle │
// │ ProductPrice │
// │ ┌────────┐ ┌────────────┐ │
// │ │ Avatar │ │ AuthorName │ │
// │ └────────┘ └────────────┘ │
// └─────────────────────────────┘
// Step 2: Build each piece
function CategoryBadge({ name }: { name: string }) {
return <span className="bg-gray-100 px-2 py-1 rounded text-sm">{name}</span>
}
function AuthorInfo({ name, avatar }: { name: string; avatar: string }) {
return (
<div className="flex items-center gap-2">
<img className="w-8 h-8 rounded-full" src={avatar} alt={name} />
<span className="text-sm">{name}</span>
</div>
)
}
// Step 3: Compose them together
function ProductCard({ product }) {
return (
<div className="border rounded-lg overflow-hidden">
<img src={product.image} alt={product.title} />
<div className="p-4">
<CategoryBadge name={product.category} />
<h2 className="text-xl font-bold mt-2">{product.title}</h2>
<p className="text-2xl font-bold mt-1">${product.price}</p>
<AuthorInfo name={product.author.name} avatar={product.author.avatar} />
</div>
</div>
)
}Data flow in React:
import { useState } from 'react'
function Parent() {
const [selectedCategory, setSelectedCategory] = useState('all')
return (
<div>
{/* Data flows DOWN via props */}
<CategoryFilter
selected={selectedCategory}
onSelect={setSelectedCategory} // Callback flows DOWN
/>
<ProductList category={selectedCategory} />
</div>
)
}
function CategoryFilter({ selected, onSelect }) {
const categories = ['all', 'Electronics', 'Accessories']
return (
<div className="flex gap-2">
{categories.map(cat => (
<button
key={cat}
className={selected === cat ? 'bg-blue-500 text-white' : 'bg-gray-200'}
onClick={() => onSelect(cat)} // Events flow UP via callbacks
>
{cat}
</button>
))}
</div>
)
}React is a UI library — it gives you components, state, and a way to render them. But to build a real application you need routing, server rendering, data fetching, optimization, and more. Next.js is a framework built on top of React that provides all of this out of the box.
| Feature | React Alone | Next.js |
|---|---|---|
| Routing | Install a router library, configure manually | Built-in file-based routing |
| Rendering | Client-side only | Server + Client rendering, streaming |
| Data Fetching | useEffect + fetch (client only) | Server Components, caching, revalidation |
| SEO | Poor (empty HTML until JS loads) | Excellent (HTML rendered on server) |
| Performance | Manual optimization | Automatic code splitting, image optimization |
| Deployment | Configure your own server | One-click deploy to Vercel |
// With plain React, data fetching happens in the browser
import { useState, useEffect } from 'react'
function BlogPage() {
const [posts, setPosts] = useState([])
const [loading, setLoading] = useState(true)
useEffect(() => {
fetch('/api/posts')
.then(res => res.json())
.then(data => {
setPosts(data)
setLoading(false)
})
}, [])
if (loading) return <p>Loading...</p>
return <PostList posts={posts} />
}