BiTree
  • Search For Lessons
  • Curriculum
  • Pricing
  • For Educators
  • Become a Tutor
  • About
  • Contact
Log InGet Started

Questions, concerns, bug reports, or suggestions? We read every message, write to us at [email protected].

More ways to reach us →
BiTree

Live coding lessons for aspiring developers and security professionals.

[email protected]

(201) 785-7951

Mon–Fri, 9 AM–5 PM EST

Learn

  • Search For Lessons
  • Curriculum
  • Pricing

Company

  • About
  • For Educators & Schools
  • Become a Tutor
  • Contact Us

Legal

  • Terms of Service
  • Privacy Policy
© 2026 BiTree. All rights reserved.
Curriculum/Web Development/React and Next.js/React Hooks
55 minBeginner

React Hooks

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.

Prerequisites:App Router: Layouts, Pages, Loading, Errors

💡 Client components only

Hooks like `useState` and `useEffect` only work in **client components** — files that opt in with `'use client'` at the top. Server components don't have state or browser APIs (we cover the distinction properly in rn-data). For this lesson, every example file starts with `'use client';`.

useState — local component state

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

useState in practice

The canonical counter. Notice how the JSX always describes the CURRENT state — you don't manipulate the DOM.

html
"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>
<button
onClick={() => setCount(count + 1)}
className="mt-2 rounded bg-blue-600 px-4 py-2 text-white"
>
Increment
</button>
</div>
);
}

useEffect — running side effects after render

`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).

useEffect — fetch data on mount, clean up subscriptions

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.

python
"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-fires
return () => {
active = false;
};
}, []); // ← run once on mount
if (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 — sharing state without prop drilling

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

useContext + createContext

Create the context, wrap your tree in a provider, read it anywhere with `useContext`.

html
"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>
);
}

Custom hooks — extract reusable logic

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.

useLocalStorage — a real custom hook

Reusable state that persists to `localStorage`. Use it in any component just like `useState` — but the value survives page reloads.

python
"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 safety
const 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 component
export default function Note() {
const [text, setText] = useLocalStorage<string>("draft", "");
return (
<textarea
value={text}
onChange={(e) => setText(e.target.value)}
className="w-full p-4 border rounded"
placeholder="Type — it survives reloads"
/>
);
}

💡 The Rules of Hooks

Two rules, enforced by the official ESLint plugin (`eslint-plugin-react-hooks`, on by default in Next.js). (1) **Only call hooks at the top level** of a function component or custom hook. Never inside if/else, loops, nested functions, or after early returns. React relies on the call ORDER being the same on every render. (2) **Only call hooks from React functions** — components (named `PascalCase`) or custom hooks (named `useXxx`). Calling from a plain helper function breaks the linter and React.

Quick Check

Why must `useState` not be called inside an `if` statement?

Pick the most accurate reason.

⚠️ Common mistakes only experienced devs catch

Five hook traps. (1) **Stale closures in useEffect** — if your effect uses a state value but doesn't list it in the deps array, you'll read the value from the FIRST render forever. Trust the ESLint warning; add the missing dep or use the functional setter (`setCount(c => c + 1)`). (2) **Infinite render loops** — calling `setState` inside the body of a component (not in a handler or effect) re-renders, which calls setState again, which re-renders... Always set state in handlers or effects. (3) **Mutating state directly** — `users.push(newUser); setUsers(users)` doesn't trigger a re-render because the reference didn't change. Always create a new array: `setUsers([...users, newUser])`. (4) **Overusing useEffect** — if a value can be computed from props or state, just compute it during render. useEffect is for SIDE EFFECTS, not derived data. (5) **Forgetting `'use client'`** — using `useState` in a server component crashes the build with a misleading error. The fix is always to add `'use client'` at the top of the file.

Sign in and purchase access to unlock this lesson.

Sign in to purchase
←App Router: Layouts, Pages, Loading, Errors
Back to React and Next.js
Server Components vs Client Components→