React & Next.js Introduction

React Fundamentals & Why Next.js

Caterpillar logo

What is React?

A JavaScript Library for Building User Interfaces

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.

Core Idea: Build your UI out of small, reusable pieces called components. Each component manages its own logic and appearance.

Why React?

  • Component-Based: Break complex UIs into small, reusable building blocks
  • Declarative: Describe what you want to see, not how to update the DOM — React handles the rest
  • Learn Once, Write Anywhere: Web (React DOM), mobile (React Native), desktop, VR, and more
  • Massive Ecosystem: Thousands of libraries, tools, and a huge community
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 />
Key Takeaway: A React component is just a JavaScript function that returns what should appear on the screen. Component names must always start with a capital letter (e.g., Welcome, not welcome) — this is how React tells them apart from regular HTML tags.

JSX: Writing UI in JavaScript

HTML-Like Syntax Inside JavaScript

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.

Key Concept: JSX is not a separate language — it gets compiled to regular JavaScript function calls. It just makes writing UI code more intuitive.
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:

  • Use { } to embed any JavaScript expression
  • Use className instead of class to set CSS classes (since class is a reserved word in JS)
  • Every list item needs a unique key prop
  • Components must return a single root element (use <> fragments to wrap multiple elements)

Components & Props

Building Blocks of Every React App

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.

Mental Model: If a component is a function, then props are its parameters. The parent decides what data to pass, the child decides how to display it.
// 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>
  )
}
Best Practice: Keep components small and focused. If a component does too many things, break it into smaller pieces.

State & Interactivity

Making Components Dynamic

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.

Key Concept: Props come from the parent (read-only). State lives inside the component (read-write). When state changes, the component re-renders.
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:

  • Always call useState at the top level of your component (never inside loops or conditions)
  • The setter function replaces the old value entirely — use spread for objects: setState({ ...state, key: newValue })
  • The setter function is the only correct way to update the state — never modify state variables directly like count = count + 1 because it won't trigger a re-render
  • State updates are batched — React processes them together for performance

useEffect & Side Effects

Running Code After Render

Side 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.

Mental Model: useEffect says "after this component renders, also do this." The dependency array controls when the effect re-runs.
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>
  )
}
Common Mistake: Forgetting the dependency array causes the effect to run after every render, which can create infinite loops with state updates inside effects.
Heads Up: In Next.js, you rarely need useEffect for data fetching — Server Components fetch data on the server before the page reaches the browser. We'll cover this in the next module!

The Prop Drilling Problem

When Passing Props Becomes Painful

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.

The Pain: A theme value defined at the top of the app has to travel through 5 components just to reach a button that actually needs it. Every middle component becomes coupled to data it doesn't care about.
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:

  • Every component in the chain gains a prop it doesn't use
  • Refactoring is painful — adding a new "global" value means touching every layer
  • Reusing components becomes harder because they all expect those extra props
Smell: If you find yourself passing the same prop through 3+ components without any of them using it, that's a sign you need Context.

React Context

Share Values Across the Tree Without Props

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.

Mental Model: Props are like handing a note to the person next to you (one hop). Context is like a PA announcement — everyone in the room hears it, no matter where they're standing.

Context has three pieces:

  • createContext(defaultValue) — defines the shape of the broadcast
  • <MyContext value={...}> — wraps the tree where the value should be available
  • use(MyContext) — reads the current value from any descendant component
import { 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>
  )
}
Result: The middle layers are clean again. They don't know or care about the theme. Only the components that actually need the value subscribe to it.

When to Reach for Context

And When to Stick with Props

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 ContextStick with Props
Theme (light / dark)A single component's title or label
Current user / auth sessionData only used by direct children
Locale / translationsFrequently changing values that re-render lots of subscribers
Currently selected item in a list/gridOne-off configuration passed once
Watch out: When the context value changes, every component that reads it re-renders. For high-frequency updates (e.g. mouse position), keep that state local or split your context into smaller, more focused pieces.
Heads Up: In Next.js, Context only works in Client Components (anything under '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.

Thinking in Components

Breaking UI Into Reusable Pieces

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.

Process: Look at any UI → draw boxes around each section → each box is a component → nest them to build the full page.
// 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:

  • Props flow down: Parent components pass data to children via props
  • Events flow up: Children notify parents about changes via callback functions
  • Single source of truth: Shared state should live in the closest common parent
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>
  )
}
The React Way (from react.dev): 1) Break UI into a component hierarchy, 2) Build a static version with no interactivity, 3) Find the minimal state needed, 4) Decide where state lives, 5) Add inverse data flow (callbacks). Start with the mockup, draw boxes, name them, then build from the smallest pieces up.

What is Next.js?

The React Framework for Production

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.

Analogy: React is the engine. Next.js is the complete car — engine + chassis + wheels + dashboard + everything you need to drive.
FeatureReact AloneNext.js
RoutingInstall a router library, configure manuallyBuilt-in file-based routing
RenderingClient-side onlyServer + Client rendering, streaming
Data FetchinguseEffect + fetch (client only)Server Components, caching, revalidation
SEOPoor (empty HTML until JS loads)Excellent (HTML rendered on server)
PerformanceManual optimizationAutomatic code splitting, image optimization
DeploymentConfigure your own serverOne-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} />
}
1 / 10
Slide 1 of 10