Topic 9 of 56 · Full Stack Advanced

Topic 9 : Hooks

Lesson TL;DRTopic 9: Hooks 📖 7 min read · 🎯 advanced · 🧭 Prerequisites: reactrouter, usinglibrariesinphp Why this matters Up until now, if you wanted your React component to remember something — like whether a...
7 min read·advanced·react · hooks · usestate · useeffect

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:

  1. 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.
  2. 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 at 0.
  • setCount replaces the state value and schedules a re-render — it does not mutate the existing value.
  • Arrow functions in onClick capture the current count from 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 whenever a or b changes.
  • 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.
  • useState manages simple local state; useEffect runs side effects after render with an optional cleanup and dependency array.
  • useContext reads context values directly, eliminating the need for <Context.Consumer> wrappers.
  • useReducer handles complex state logic through dispatched action objects and a pure reducer function.

📚 Further Reading

Like this topic? It’s one of 56 in Full Stack Advanced.

Block your seat for ₹2,500 and join the next cohort.