Introduction to React

Build UIs from components. The library that changed how we think about frontends.

Why React

Declarative UI, component reuse, and a massive ecosystem.

// The problem React solves:
// Vanilla JS: you tell the DOM WHAT TO DO (imperative)
document.querySelector(".count").textContent = count;
document.querySelector(".btn").addEventListener("click", ...);
// You manage every DOM update manually. Complex UIs = spaghetti.

// React: you describe WHAT IT SHOULD LOOK LIKE (declarative)
function Counter() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount(count + 1)}>+</button>
    </div>
  );
}
// React figures out what DOM changes are needed.

// React's mental model:
// UI = f(state)
// Your UI is a function of your data.
// When data changes → React re-renders → DOM updates.

// Why React dominates:
// - Component model (reusable UI pieces)
// - Declarative (describe what, not how)
// - Virtual DOM (efficient updates)
// - Massive ecosystem (Next.js, React Native, etc.)
// - Job market (most in-demand frontend skill)

// Setup:
// $ pnpm create vite my-app --template react-ts
// $ cd my-app && pnpm install && pnpm dev

Components & JSX

Functions that return UI. JSX makes HTML-in-JS natural.

// A component is just a function that returns JSX:
function Greeting() {
  return <h1>Hello, World!</h1>;
}

// JSX looks like HTML but it's JavaScript:
// <h1>Hello</h1> compiles to:
// React.createElement("h1", null, "Hello")

// JSX rules:
// 1. Return ONE root element (or use Fragment <>...</>)
function Card() {
  return (
    <>   {/* Fragment — no extra DOM node */}
      <h2>Title</h2>
      <p>Content</p>
    </>
  );
}

// 2. Close all tags (even self-closing)
// <img src="..." />   ← not <img src="...">
// <br />              ← not <br>

// 3. className instead of class
// <div className="container">  ← not class="container"

// 4. JavaScript expressions in curly braces:
const name = "Alice";
const element = <h1>Hello, {name}!</h1>;
const math = <p>{2 + 2}</p>; // renders: 4

Components are functions returning JSX. JSX is syntactic sugar over React.createElement — it's JavaScript, not HTML.

1 / 2

Props

Pass data from parent to child. One-way data flow.

// Props = arguments to your component function:
function Button({ label, onClick, variant = "primary" }) {
  return (
    <button className={variant} onClick={onClick}>
      {label}
    </button>
  );
}

// Using it:
<Button label="Save" onClick={handleSave} variant="primary" />
<Button label="Cancel" onClick={handleCancel} variant="secondary" />

// Props are READ-ONLY — never modify them:
function Bad({ items }) {
  items.push("new"); // ❌ NEVER mutate props!
}

// children prop — content between tags:
function Card({ title, children }) {
  return (
    <div className="card">
      <h2>{title}</h2>
      <div className="card-body">{children}</div>
    </div>
  );
}

// Usage:
<Card title="User Profile">
  <p>This is the card content.</p>
  <button>Edit</button>
</Card>

// TypeScript props:
interface ButtonProps {
  label: string;
  onClick: () => void;
  variant?: "primary" | "secondary";
  disabled?: boolean;
}

function Button({ label, onClick, variant = "primary", disabled }: ButtonProps) {
  return <button className={variant} onClick={onClick} disabled={disabled}>{label}</button>;
}

// Data flows DOWN (parent → child)
// Events flow UP (child → parent via callbacks)
// This is "one-way data flow" — predictable and debuggable

State

Data that changes over time. When state updates, React re-renders.

// useState — the most fundamental hook:
import { useState } from "react";

function Counter() {
  const [count, setCount] = useState(0);
  //     ↑ value  ↑ updater    ↑ initial value

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>+</button>
      <button onClick={() => setCount(count - 1)}>-</button>
      <button onClick={() => setCount(0)}>Reset</button>
    </div>
  );
}

// State rules:
// 1. Call hooks at the TOP of your component (not inside if/loops)
// 2. State updates trigger a re-render
// 3. State is preserved between renders
// 4. Each component instance has its own state

// Object state — always spread to create new reference:
const [user, setUser] = useState({ name: "", email: "" });

// ❌ BAD — mutating existing object:
user.name = "Alice";
setUser(user); // React won't re-render (same reference!)

// ✓ GOOD — new object:
setUser({ ...user, name: "Alice" });

// Array state:
const [items, setItems] = useState([]);

// Add:
setItems([...items, newItem]);
// Remove:
setItems(items.filter(i => i.id !== targetId));
// Update one:
setItems(items.map(i => i.id === targetId ? { ...i, done: true } : i));

// Functional updates (when new state depends on old):
setCount(prev => prev + 1); // ✓ always gets latest value

Rendering

How React decides what to update in the DOM.

// React rendering cycle:
// 1. State changes (setState called)
// 2. React calls your component function again
// 3. Returns new JSX (virtual DOM)
// 4. React DIFFS old vs new virtual DOM
// 5. Only changed DOM nodes are updated (reconciliation)

// Example:
function App() {
  const [name, setName] = useState("World");
  console.log("App rendered!"); // logs on every state change

  return (
    <div>
      <h1>Hello, {name}!</h1>           {/* updates text node only */}
      <input onChange={e => setName(e.target.value)} />
      <ExpensiveList />                  {/* re-renders too! */}
    </div>
  );
}

// Hooks overview (the essential ones):
// useState  — state that triggers re-renders
// useEffect — side effects (fetch data, subscriptions)
// useRef    — mutable value that doesn't trigger re-render
// useMemo   — cache expensive computations
// useCallback — cache function references

// useEffect — run code after render:
import { useEffect } from "react";

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    // Runs after component renders:
    fetch("/api/users/" + userId)
      .then(r => r.json())
      .then(data => setUser(data));

    // Cleanup (runs before next effect or unmount):
    return () => { /* cancel subscription, etc. */ };
  }, [userId]); // ← dependency array: re-run when userId changes

  if (!user) return <p>Loading...</p>;
  return <h1>{user.name}</h1>;
}

// Dependency array rules:
// []         — run once on mount only
// [a, b]    — run when a or b changes
// (omitted)  — run after every render (rarely want this)

FAQ

Common questions about React.