← Back to the Build Your Homepage series
⚛️
EPISODE 01
JSX · props · function components · useState · side effects · cleanup · dependencies · data fetching · Prop drilling · Context.Provider · custom provider · Reducer pattern · dispatch · actions · when over useState

Components & useState · useEffect · useContext · useReducer

Compose UI from reusable components. Pass data with props, manage local state with useState, and re-render automatically when state changes. Run side effects (data fetches, subscriptions, timers) outside the render cycle. Learn the dependency array, cleanup, and how to fetch data correctly. Stop drilling props through layers. Use Context to share theme, auth, or i18n state — wrapped in a clean custom provider component. Handle complex state with useReducer — a tiny reducer function and dispatched actions. Predictable, testable, and great for multi-step state.

ReactcomponentspropsuseStateJSXuseEffectside effectcleanupdependenciesContextuseContextProviderglobal stateuseReducerdispatchactionsRedux pattern
Duration
⏱ About 2 hours + About 2 hours + About 1.5 hours + About 2 hours
Level
📊 Intermediate
Prerequisite
🎯 Lesson 5 + JS basics
OUTCOME
A small React app with stateful, prop-driven components / Fetch and subscribe to data with proper cleanup / A theme/auth context shared across an app without prop drilling / A to-do list managed by a typed reducer with action types

What you'll learn

  • 1Write function components with JSX
  • 2Pass data with typed props
  • 3Manage local state with useState
  • 4Lift state up when two siblings need it
  • 5Use useEffect with the right dependency array
  • 6Return a cleanup function for subscriptions
  • 7Fetch data in an effect (or prefer Suspense / server components)
  • 8Avoid effect anti-patterns
  • 9Create a Context with a default value
  • 10Build a custom Provider component
  • 11Consume the context with useContext
  • 12Know when NOT to use Context (frequent updates)
  • 13Write a reducer function: (state, action) => newState
  • 14Dispatch actions instead of calling many setters
  • 15Use a discriminated-union Action type for type safety
  • 16Choose useReducer over useState for complex, interrelated state

Components & useState

Compose UI from reusable components. Pass data with props, manage local state with useState, and re-render automatically when state changes.

1. Function Components

tsx
type CardProps = { title: string; description: string };

export default function Card({ title, description }: CardProps) {
  return (
    <div className="rounded-xl border p-4">
      <h2 className="text-lg font-bold">{title}</h2>
      <p className="text-gray-600">{description}</p>
    </div>
  );
}

2. JSX Rules

  • className instead of class
  • Self-closing tags need a closing slash (<br />)
  • JS expressions in { } — including conditionals and array.map
  • Return one root element (use <>...</> fragment when needed)
tsx
{count > 0 && <p>Items: {count}</p>}
{items.map(i => <li key={i.id}>{i.name}</li>)}
<>multiple <strong>elements</strong></>

3. useState

tsx
"use client";
import { useState } from "react";

export function Counter() {
  const [count, setCount] = useState(0);
  return (
    <button onClick={() => setCount(count + 1)}>Count: {count}</button>
  );
}
⚠️

Never mutate state. setCount(count + 1) is correct; count = count + 1 silently breaks rerendering.

4. Lifting State Up

When two sibling components need the same state, move it up to their common parent and pass it down via props.

tsx
function App() {
  const [filter, setFilter] = useState("");
  return (
    <>
      <SearchBar value={filter} onChange={setFilter} />
      <ItemList filter={filter} />
    </>
  );
}

useEffect

Run side effects (data fetches, subscriptions, timers) outside the render cycle. Learn the dependency array, cleanup, and how to fetch data correctly.

1. Basic Effect

tsx
useEffect(() => {
  document.title = `${count} clicks`;
}, [count]);  // runs after every render where count changed
  • Empty deps [] → runs once on mount
  • No deps → runs after every render (rarely what you want)
  • Deps [a, b] → runs when a or b changes

2. Cleanup

tsx
useEffect(() => {
  const id = setInterval(() => console.log("tick"), 1000);
  return () => clearInterval(id);    // cleanup on unmount/dep change
}, []);

Cleanup runs before the next effect fires and on unmount — essential for timers and subscriptions.

3. Data Fetching

tsx
function Users() {
  const [users, setUsers] = useState<User[] | null>(null);

  useEffect(() => {
    const controller = new AbortController();
    (async () => {
      const res = await fetch("/api/users", { signal: controller.signal });
      const data = await res.json();
      setUsers(data);
    })().catch(err => { if (err.name !== "AbortError") console.error(err); });
    return () => controller.abort();
  }, []);

  if (!users) return <p>Loading...</p>;
  return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}

4. Anti-patterns

  • Setting state in an effect that immediately re-triggers itself (infinite loop)
  • Forgetting deps and using stale state
  • Using an effect to derive state — derive in render instead
  • Subscribing without cleanup
💡

Many "do I need an effect?" answers are no. Read the React docs page "You Might Not Need an Effect".

useContext

Stop drilling props through layers. Use Context to share theme, auth, or i18n state — wrapped in a clean custom provider component.

1. Create the Context

tsx
import { createContext, useContext, useState } from "react";

type Theme = "light" | "dark";
const ThemeContext = createContext<{
  theme: Theme;
  toggle: () => void;
} | null>(null);

export function ThemeProvider({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useState<Theme>("light");
  const toggle = () => setTheme(t => t === "light" ? "dark" : "light");
  return (
    <ThemeContext.Provider value={{ theme, toggle }}>
      {children}
    </ThemeContext.Provider>
  );
}

export function useTheme() {
  const ctx = useContext(ThemeContext);
  if (!ctx) throw new Error("useTheme must be used inside ThemeProvider");
  return ctx;
}

2. Provide and Consume

tsx
// app/layout.tsx
<ThemeProvider>
  <App />
</ThemeProvider>

// Anywhere in the tree
function Header() {
  const { theme, toggle } = useTheme();
  return <button onClick={toggle}>Theme: {theme}</button>;
}

3. Context Gotchas

  • Every consumer re-renders when the context value changes
  • Split fast-changing state into a separate context (or use a state library)
  • Memoize the provider value with useMemo when it contains objects

useReducer

Handle complex state with useReducer — a tiny reducer function and dispatched actions. Predictable, testable, and great for multi-step state.

1. Reducer Shape

tsx
type State = { count: number; step: number };
type Action =
  | { type: "increment" }
  | { type: "decrement" }
  | { type: "set-step"; step: number }
  | { type: "reset" };

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case "increment": return { ...state, count: state.count + state.step };
    case "decrement": return { ...state, count: state.count - state.step };
    case "set-step":  return { ...state, step: action.step };
    case "reset":     return { count: 0, step: 1 };
  }
}

2. Use the Reducer

tsx
function Counter() {
  const [state, dispatch] = useReducer(reducer, { count: 0, step: 1 });
  return (
    <div>
      <p>{state.count}</p>
      <button onClick={() => dispatch({ type: "increment" })}>+</button>
      <button onClick={() => dispatch({ type: "decrement" })}>-</button>
      <input type="number" value={state.step}
             onChange={e => dispatch({ type: "set-step", step: +e.target.value })} />
      <button onClick={() => dispatch({ type: "reset" })}>reset</button>
    </div>
  );
}

3. useState vs useReducer

PickWhen
useState1–2 simple values, updates are independent
useReducerMultiple values that change together, complex update logic, you want to test the reducer in isolation
Example code / lecture materials

All lecture materials and example code are openly available on GitHub.

View on GitHub ↗

Comments 0

No comments yet. Be the first.

Only members can comment.

Comments with ads, abuse, personal information or illegal content are hidden or removed. Comments are shown with your nickname. Privacy Policy