After this lesson, you will be able to: Use the four React hooks every working developer touches daily — useState, useEffect, useContext, and your own custom hooks — and explain when (and when not) to reach for each.
Hooks are how function components in React access state, side effects, context, and reusable logic. They arrived in React 16.8 (2019) and replaced the class-component style entirely for new code. Every Next.js codebase you'll work in is built on hooks. This lesson takes you from never having written one to comfortable with the four you'll use every week, plus the Rules of Hooks that keep them sane.
`useState` gives a component a piece of memory that survives across renders. Each call returns the current value and a setter; calling the setter triggers a re-render. Mental model: when state changes, React calls your function again, and you describe what the UI should look like for the new state. The state-update-causes-re-render loop is the entire React mental model.
The canonical counter. Notice how the JSX always describes the CURRENT state — you don't manipulate the DOM.
"use client";import { useState } from "react";export default function Counter() {const [count, setCount] = useState(0);return (<div className="p-8"><p className="text-2xl">Count: {count}</p><buttononClick={() => setCount(count + 1)}className="mt-2 rounded bg-blue-600 px-4 py-2 text-white">Increment</button></div>);}
`useEffect` runs code AFTER React renders your component. Use it for anything that touches the world outside React: fetching data, subscribing to events, setting up timers, syncing with a third-party widget. The second argument is the **dependency array**. Effects re-run whenever a value in the array changes. `[]` means 'run once on mount.' Omitting the array means 'run after every render' (almost always wrong).
Two patterns: fetch on mount, and subscribe + clean up. The returned function is the cleanup; React runs it before the next effect or on unmount.
"use client";import { useEffect, useState } from "react";export default function Users() {const [users, setUsers] = useState<{ id: number; name: string }[]>([]);const [status, setStatus] = useState<"loading" | "ready" | "error">("loading");// Fetch on mount (empty deps array)useEffect(() => {let active = true;fetch("https://jsonplaceholder.typicode.com/users").then((r) => r.json()).then((data) => {if (!active) return;setUsers(data);setStatus("ready");}).catch(() => active && setStatus("error"));// Cleanup runs on unmount OR before the effect re-firesreturn () => {active = false;};}, []); // ← run once on mountif (status === "loading") return <p>Loading...</p>;if (status === "error") return <p>Could not load.</p>;return (<ul>{users.map((u) => (<li key={u.id}>{u.name}</li>))}</ul>);}
`useContext` gives any component in a subtree access to a shared value without passing it down through every intermediate component. Use it for app-wide concerns: the current user, theme, locale, feature flags. Don't use it for everything — passing props is fine when the depth is shallow, and overusing context makes components hard to test in isolation.
Create the context, wrap your tree in a provider, read it anywhere with `useContext`.
"use client";import { createContext, useContext, useState } from "react";type Theme = "light" | "dark";const ThemeContext = createContext<{theme: Theme;setTheme: (t: Theme) => void;}>({ theme: "light", setTheme: () => {} });export function ThemeProvider({ children }: { children: React.ReactNode }) {const [theme, setTheme] = useState<Theme>("light");return (<ThemeContext.Provider value={{ theme, setTheme }}><div data-theme={theme}>{children}</div></ThemeContext.Provider>);}export function ThemeToggle() {const { theme, setTheme } = useContext(ThemeContext);return (<button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>Switch to {theme === "light" ? "dark" : "light"}</button>);}
A custom hook is just a function whose name starts with `use` and which calls other hooks. The naming convention is what makes React's linter recognize it as a hook (and enforce the rules). If two components share the same useState + useEffect pattern, extract it to a hook. The component becomes a one-liner; the logic is reusable and testable on its own.
Reusable state that persists to `localStorage`. Use it in any component just like `useState` — but the value survives page reloads.
"use client";import { useEffect, useState } from "react";export function useLocalStorage<T>(key: string, initial: T) {const [value, setValue] = useState<T>(() => {if (typeof window === "undefined") return initial; // SSR safetyconst stored = window.localStorage.getItem(key);return stored ? (JSON.parse(stored) as T) : initial;});useEffect(() => {window.localStorage.setItem(key, JSON.stringify(value));}, [key, value]);return [value, setValue] as const;}// Use it in any componentexport default function Note() {const [text, setText] = useLocalStorage<string>("draft", "");return (<textareavalue={text}onChange={(e) => setText(e.target.value)}className="w-full p-4 border rounded"placeholder="Type — it survives reloads"/>);}
Pick the most accurate reason.
Sign in and purchase access to unlock this lesson.