Topic 9: Hooks
📖 7 min read · 🎯 advanced · 🧭 Prerequisites: react-router, using-libraries-in-php
Why this matters
Up until now, if you wanted your React component to remember something — like whether a button was clicked, or what a user typed — you had to write a full class component. Lots of boilerplate, lots of confusion for beginners. Then React 16.8 arrived and changed everything with hooks. Now a simple function can track state, respond to side effects, and tap into shared data — no class required. In this lesson, we're going to learn useState, useEffect, useContext, and useReducer, the hooks you'll use in almost every real React project you build.
What You'll Learn
- How to follow the two core Rules of Hooks to avoid subtle bugs
- How to add local state to a functional component with
useState - How to run side effects (data fetching, DOM changes) with
useEffect - How to consume React context without wrapping components in a consumer, using
useContext - How to manage complex state transitions with
useReducer
The Analogy
Think of a functional component as a blank recipe card — it describes what to cook, but has nowhere to write notes about what happened last time you made it. Hooks are sticky tabs you attach to that card: one tab tracks how many times you've made the dish (useState), another fires a reminder to preheat the oven before you start (useEffect), a third fetches the house-wide spice preferences from a shared pantry without rummaging through every cupboard (useContext), and a fourth lets you handle multi-step prep instructions through a dispatcher rather than a tangled series of if statements (useReducer). The recipe card stays clean; the tabs carry all the dynamic memory.
Chapter 1: Introduction to Hooks
Hooks are functions that let you use state and other React features without writing a class. They were introduced in React 16.8 and are now the standard way to build components. The two most commonly used hooks are useState and useEffect, but the ecosystem includes many more.
Rules of Hooks
These two rules are enforced by the eslint-plugin-react-hooks linter and must never be violated:
- Only Call Hooks at the Top Level — never call hooks inside loops, conditions, or nested functions. React depends on the order hooks are called to associate state with the right variable between renders.
- Only Call Hooks from React Functions — call them from React functional components or from custom hooks. Never from plain JavaScript functions or class components.
Violating either rule produces bugs that are infamously hard to diagnose because React silently associates state with the wrong hook call.
Chapter 2: Using the useState Hook
useState lets you add a piece of reactive state to a functional component. Calling it returns a tuple: the current state value and a setter function that triggers a re-render when called.
const [stateValue, setStateValue] = useState(initialValue);
Example: Counter Component with useState
The class wired up a simple counter as their first real-world demonstration.
src/components/Counter.js:
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<h1>Count: {count}</h1>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={() => setCount(count - 1)}>Decrement</button>
</div>
);
}
export default Counter;
Key observations:
useState(0)seeds the initial count at0.setCountreplaces the state value and schedules a re-render — it does not mutate the existing value.- Arrow functions in
onClickcapture the currentcountfrom the closure.
Chapter 3: Using the useEffect Hook
useEffect lets you run side effects inside a functional component — things like data fetching, setting up subscriptions, or manually updating the DOM. It runs after the browser has painted the screen.
useEffect(() => {
// side effect here
return () => { /* optional cleanup */ };
}, [/* dependency array */]);
The dependency array controls when the effect re-runs:
[]— run once after the initial mount only.[a, b]— re-run wheneveraorbchanges.- Omitted — re-run after every render (rarely what you want).
Example: Fetching Data with useEffect
The class built a component that fetches a post from the JSONPlaceholder API.
src/components/FetchData.js:
import React, { useState, useEffect } from 'react';
function FetchData() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch('https://jsonplaceholder.typicode.com/posts/1')
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
setData(data);
setLoading(false);
})
.catch(error => {
setError(error);
setLoading(false);
});
}, []); // The empty array ensures the effect runs only once
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<h2>{data.title}</h2>
<p>{data.body}</p>
</div>
);
}
export default FetchData;
Three parallel state slices — data, loading, and error — cover every rendering branch cleanly. The empty dependency array [] means the fetch fires once on mount and never again.
Chapter 4: Using the useContext Hook
useContext lets you read a React context value directly inside a functional component, without wrapping it in a <Context.Consumer> render-prop. This eliminates a layer of JSX nesting for every consumer.
Example: Theme Context with useContext
The class built a theme system with a provider, a custom hook, and a consumer component.
src/contexts/ThemeContext.js:
import React, { createContext, useContext, useState } from 'react';
const ThemeContext = createContext();
export const ThemeProvider = ({ children }) => {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
};
export const useTheme = () => useContext(ThemeContext);
useTheme is a custom hook — a plain function that calls useContext internally. It hides the context object from consumers and gives autocomplete a clean name to work with.
src/components/ThemeSwitcher.js:
import React from 'react';
import { useTheme } from '../contexts/ThemeContext';
function ThemeSwitcher() {
const { theme, setTheme } = useTheme();
return (
<div>
<p>Current theme: {theme}</p>
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Toggle Theme
</button>
</div>
);
}
export default ThemeSwitcher;
Integrating ThemeProvider and ThemeSwitcher
The class wrapped the application in ThemeProvider so every descendant can access the theme without prop drilling.
src/App.js:
import React from 'react';
import './App.css';
import { ThemeProvider } from './contexts/ThemeContext';
import ThemeSwitcher from './components/ThemeSwitcher';
function App() {
return (
<ThemeProvider>
<div className="App">
<h1>React Hooks Example</h1>
<ThemeSwitcher />
</div>
</ThemeProvider>
);
}
export default App;
Chapter 5: Using the useReducer Hook
useReducer is an alternative to useState for state that involves multiple sub-values or complex transition logic. Instead of calling a setter directly, you dispatch typed action objects to a pure reducer function.
const [state, dispatch] = useReducer(reducer, initialState);
This mirrors the Redux pattern and makes state transitions explicit, testable, and easy to read in isolation.
Example: Counter with useReducer
The class rebuilt the counter using useReducer to demonstrate the pattern.
src/components/ReducerCounter.js:
import React, { useReducer } from 'react';
const initialState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
return state;
}
}
function ReducerCounter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<h1>Count: {state.count}</h1>
<button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
<button onClick={() => dispatch({ type: 'decrement' })}>Decrement</button>
</div>
);
}
export default ReducerCounter;
reducer is a pure function — given the same state and action it always returns the same result, making it trivial to unit-test without mounting a component.
Integrating All Components
The class assembled the full application with all four hooks in play at once.
src/App.js:
import React from 'react';
import './App.css';
import Counter from './components/Counter';
import FetchData from './components/FetchData';
import ReducerCounter from './components/ReducerCounter';
import { ThemeProvider } from './contexts/ThemeContext';
import ThemeSwitcher from './components/ThemeSwitcher';
function App() {
return (
<ThemeProvider>
<div className="App">
<h1>React Hooks Example</h1>
<Counter />
<FetchData />
<ReducerCounter />
<ThemeSwitcher />
</div>
</ThemeProvider>
);
}
export default App;
flowchart TD
App["App (ThemeProvider wraps all)"]
App --> Counter["Counter\nuseState"]
App --> FetchData["FetchData\nuseState + useEffect"]
App --> ReducerCounter["ReducerCounter\nuseReducer"]
App --> ThemeSwitcher["ThemeSwitcher\nuseContext → useTheme"]
ThemeContext["ThemeContext\n(createContext)"] -->|provides theme + setTheme| ThemeSwitcher
🧪 Try It Yourself
Task: Build a useFetch custom hook that extracts the fetch logic from FetchData into a reusable function.
What to build: A hook useFetch(url) that returns { data, loading, error }. Then rewrite FetchData to use it with a single line.
Starter snippet:
// src/hooks/useFetch.js
import { useState, useEffect } from 'react';
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// TODO: fetch from url, update data/loading/error
}, [url]);
return { data, loading, error };
}
export default useFetch;
Success criterion: FetchData should shrink to something like:
const { data, loading, error } = useFetch('https://jsonplaceholder.typicode.com/posts/1');
…and still render the post title and body correctly in the browser.
🔍 Checkpoint Quiz
Q1. Which of the following correctly describes the Rules of Hooks?
A) Hooks can be called inside if statements as long as the condition is always true
B) Hooks must be called at the top level of a React function and only from React functions
C) Hooks can be called from regular JavaScript utility functions
D) Hooks must be called inside class component render methods
Q2. Given this snippet, what does the browser display after the button is clicked twice?
const [count, setCount] = useState(5);
// ...
<button onClick={() => setCount(count - 1)}>Down</button>
<h1>{count}</h1>
A) 5
B) 4
C) 3
D) 0
Q3. What is wrong with the following useEffect usage?
useEffect(() => {
fetch('/api/user').then(r => r.json()).then(setUser);
});
A) fetch cannot be used inside useEffect
B) The missing dependency array means the effect re-runs after every render, causing an infinite fetch loop
C) setUser must be called with a callback instead of a direct reference
D) Nothing is wrong — this is the correct pattern
Q4. You have a shopping cart with actions ADD_ITEM, REMOVE_ITEM, and CLEAR_CART, each modifying the same items array. Should you use useState or useReducer, and why?
A1. B — Hooks must be called at the top level and only from React functions. Calling them inside conditions or loops breaks React's ability to associate state with the correct hook call across renders.
A2. C — count starts at 5. First click → 4, second click → 3. The <h1> shows 3.
A3. B — Without a dependency array, useEffect runs after every render. The fetch completes, calls setUser, which triggers a re-render, which fires the effect again — an infinite loop. Adding [] fixes it by running the effect only on mount.
A4. useReducer is the better fit. Three distinct action types modifying the same piece of state is exactly the complex-transition scenario useReducer is designed for. Each case in the reducer is isolated and easily unit-tested; the same logic in useState would require multiple setters or deeply nested conditionals spread across event handlers.
🪞 Recap
- Hooks let you use state and React features in functional components without writing classes — introduced in React 16.8.
- Always call hooks at the top level of a React function, never inside loops, conditions, or nested functions.
useStatemanages simple local state;useEffectruns side effects after render with an optional cleanup and dependency array.useContextreads context values directly, eliminating the need for<Context.Consumer>wrappers.useReducerhandles complex state logic through dispatched action objects and a pure reducer function.
📚 Further Reading
- React Hooks Reference — official docs — the canonical source for every built-in hook's API and behavior
- Rules of Hooks — React docs — why the rules exist and how the linter enforces them
- useHooks pattern library — a community collection of production-ready custom hooks worth reading for idioms
- ⬅️ Previous: Using Libraries in PHP
- ➡️ Next: Navigation