React Tutorial 0/120 lessons ~6 min read Lesson 44

    Real Hook Examples

    Real Hook examples combine useState, useEffect, useRef, useMemo, useCallback, useReducer, and custom Hooks into practical application patterns.

    Course progress0%
    Focus
    34 guided sections
    Practice signal
    Examples included
    Career prep
    Interview Q&A included

    Introduction

    React Hooks become much easier to understand when you see how they solve real application problems.

    So far, you have learned individual Hooks such as:

    • useState
    • useEffect
    • useRef
    • useMemo
    • useCallback
    • useReducer
    • Custom Hooks

    The next step is to combine them correctly.

    Real applications rarely use a Hook in isolation. A search box may use state, an Effect, a ref, and a Custom Hook. A form may use state or a reducer. A data table may use memoization. A modal may use refs and Effects.

    This lesson brings those ideas together through practical examples.

    The goal is not to memorize patterns. The goal is to recognize which Hook solves which problem and how Hooks work together while keeping components predictable.

    1. A Practical Hook Decision Process

    Before adding a Hook, ask:

    text
    What problem am I solving?
    |
    +-- UI data changes?
    | -> useState / useReducer
    |
    +-- External system synchronization?
    | -> useEffect
    |
    +-- Need a DOM node or mutable value?
    | -> useRef
    |
    +-- Expensive calculation?
    | -> useMemo
    |
    +-- Stable callback identity?
    | -> useCallback
    |
    +-- Reusable stateful logic?
    | -> Custom Hook

    This prevents the common mistake of choosing a Hook first and then trying to find a reason to use it.

    Example: Counter with Functional Updates

    A counter is simple, but it demonstrates an important state pattern.

    javascript
    import { useState } from "react";
    function Counter() {
    const [count, setCount] = useState(0);
    function increment() {
    setCount((currentCount) => currentCount + 1);
    }
    return (
    <div>
    <p>Count: {count}</p>
    <button onClick={increment}>Increase</button>
    </div>
    );
    }
    export default Counter;

    Why use a functional update?

    The next state depends on the previous state:

    javascript
    setCount((currentCount) => currentCount + 1);

    This is preferable to manually depending on a potentially stale value when multiple updates are involved.

    For example:

    javascript
    function increaseThreeTimes() {
    setCount((value) => value + 1);
    setCount((value) => value + 1);
    setCount((value) => value + 1);
    }

    Each update receives the latest pending state.

    Example: Form State with `useState`

    For a small form, separate state variables can be clear and straightforward.

    javascript
    import { useState } from "react";
    function LoginForm() {
    const [email, setEmail] = useState("");
    const [password, setPassword] = useState("");
    function handleSubmit(event) {
    event.preventDefault();
    console.log({
    email,
    password,
    });
    }
    return (
    <form onSubmit={handleSubmit}>
    <input
    type="email"
    value={email}
    onChange={(event) => setEmail(event.target.value)}
    placeholder="Email"
    />
    <input
    type="password"
    value={password}
    onChange={(event) => setPassword(event.target.value)}
    placeholder="Password"
    />
    <button type="submit">Login</button>
    </form>
    );
    }
    export default LoginForm;

    This is a controlled form.

    The React state represents the current value of each input.

    Important principle

    Do not create an Effect just to react to a button click:

    javascript
    // Usually unnecessary
    useEffect(() => {
    if (submitted) {
    submitForm();
    }
    }, [submitted]);

    The user action already happened in the event handler.

    Prefer:

    javascript
    function handleSubmit(event) {
    event.preventDefault();
    submitForm();
    }

    Example: Search Input with Derived Data

    Suppose a list is already available in memory and you want to filter it based on a search term.

    javascript
    import { useState } from "react";
    const products = [
    { id: 1, name: "Laptop" },
    { id: 2, name: "Keyboard" },
    { id: 3, name: "Mouse" },
    { id: 4, name: "Monitor" },
    ];
    function ProductSearch() {
    const [search, setSearch] = useState("");
    const filteredProducts = products.filter((product) =>
    product.name.toLowerCase().includes(search.toLowerCase())
    );
    return (
    <div>
    <input
    value={search}
    onChange={(event) => setSearch(event.target.value)}
    placeholder="Search products"
    />
    <ul>
    {filteredProducts.map((product) => (
    <li key={product.id}>{product.name}</li>
    ))}
    </ul>
    </div>
    );
    }
    export default ProductSearch;

    Notice that filteredProducts is derived data.

    We do not need:

    javascript
    const [filteredProducts, setFilteredProducts] = useState([]);

    and we do not need an Effect to keep it synchronized.

    The source of truth is:

    javascript
    search

    The filtered list is calculated from that state.

    Example: Fetching Data with `useEffect`

    Fetching data is a classic example of synchronizing with an external system.

    javascript
    import { useEffect, useState } from "react";
    function Users() {
    const [users, setUsers] = useState([]);
    const [status, setStatus] = useState("loading");
    useEffect(() => {
    let ignore = false;
    async function loadUsers() {
    try {
    const response = await fetch(
    "https://jsonplaceholder.typicode.com/users"
    );
    if (!response.ok) {
    throw new Error("Failed to load users");
    }
    const data = await response.json();
    if (!ignore) {
    setUsers(data);
    setStatus("success");
    }
    } catch (error) {
    if (!ignore) {
    setStatus("error");
    }
    }
    }
    loadUsers();
    return () => {
    ignore = true;
    };
    }, []);
    if (status === "loading") {
    return <p>Loading...</p>;
    }
    if (status === "error") {
    return <p>Unable to load users.</p>;
    }
    return (
    <ul>
    {users.map((user) => (
    <li key={user.id}>{user.name}</li>
    ))}
    </ul>
    );
    }
    export default Users;

    The cleanup prevents an outdated request from updating the component after the Effect has been cleaned up.

    For production applications, you may also use request cancellation with AbortController or a data-fetching library.

    Example: Search with an External API

    Now combine state and Effects.

    javascript
    import { useEffect, useState } from "react";
    function UserSearch() {
    const [query, setQuery] = useState("");
    const [users, setUsers] = useState([]);
    const [status, setStatus] = useState("idle");
    useEffect(() => {
    if (!query.trim()) {
    setUsers([]);
    setStatus("idle");
    return;
    }
    const controller = new AbortController();
    async function searchUsers() {
    setStatus("loading");
    try {
    const response = await fetch(
    `https://jsonplaceholder.typicode.com/users?name_like=${encodeURIComponent(
    query
    )}`,
    {
    signal: controller.signal,
    }
    );
    if (!response.ok) {
    throw new Error("Search failed");
    }
    const data = await response.json();
    setUsers(data);
    setStatus("success");
    } catch (error) {
    if (error.name !== "AbortError") {
    setStatus("error");
    }
    }
    }
    searchUsers();
    return () => {
    controller.abort();
    };
    }, [query]);
    return (
    <div>
    <input
    value={query}
    onChange={(event) => setQuery(event.target.value)}
    placeholder="Search users"
    />
    {status === "loading" && <p>Searching...</p>}
    {status === "error" && <p>Search failed.</p>}
    <ul>
    {users.map((user) => (
    <li key={user.id}>{user.name}</li>
    ))}
    </ul>
    </div>
    );
    }
    export default UserSearch;

    What is happening?

    text
    User types
    |
    v
    query changes
    |
    v
    Component renders
    |
    v
    Effect sees new query
    |
    v
    API request starts
    |
    +--> cleanup cancels previous request
    |
    v
    response arrives
    |
    v
    state updates
    |
    v
    component renders again

    This is a useful real-world state/effect relationship.

    Example: Debounced Search with `useEffect`

    Searching on every keystroke can generate too many requests.

    A simple debounce can delay the API request.

    javascript
    import { useEffect, useState } from "react";
    function SearchBox() {
    const [query, setQuery] = useState("");
    const [debouncedQuery, setDebouncedQuery] = useState("");
    useEffect(() => {
    const timer = setTimeout(() => {
    setDebouncedQuery(query);
    }, 400);
    return () => {
    clearTimeout(timer);
    };
    }, [query]);
    return (
    <div>
    <input
    value={query}
    onChange={(event) => setQuery(event.target.value)}
    placeholder="Search..."
    />
    <p>Searching for: {debouncedQuery}</p>
    </div>
    );
    }
    export default SearchBox;

    The cleanup clears the previous timer whenever the query changes.

    This demonstrates an important Effect pattern:

    javascript
    setup();
    return cleanup;

    The cleanup should undo or cancel the work established by the Effect.

    Example: Focus an Input with `useRef`

    Refs are useful when you need access to a DOM node.

    javascript
    import { useRef } from "react";
    function SearchForm() {
    const inputRef = useRef(null);
    function focusInput() {
    inputRef.current?.focus();
    }
    return (
    <div>
    <input ref={inputRef} placeholder="Search..." />
    <button onClick={focusInput}>Focus Search</button>
    </div>
    );
    }
    export default SearchForm;

    The ref stores the DOM element:

    text
    inputRef
    |
    v
    <input />

    Changing inputRef.current does not trigger a render.

    That is why a ref is appropriate for imperative DOM operations.

    Example: Focus After a Condition Changes

    Sometimes a state change should result in an interaction with the DOM.

    javascript
    import { useEffect, useRef, useState } from "react";
    function EditableTitle() {
    const [editing, setEditing] = useState(false);
    const inputRef = useRef(null);
    useEffect(() => {
    if (editing) {
    inputRef.current?.focus();
    }
    }, [editing]);
    return (
    <div>
    {editing ? (
    <input ref={inputRef} defaultValue="My title" />
    ) : (
    <button onClick={() => setEditing(true)}>
    Edit title
    </button>
    )}
    </div>
    );
    }
    export default EditableTitle;

    Here the Effect has a legitimate synchronization role:

    text
    React state: editing
    |
    v
    DOM should be focused
    |
    v
    Effect performs focus()

    Example: Expensive Calculation with `useMemo`

    Imagine a component displaying thousands of products.

    javascript
    import { useMemo, useState } from "react";
    function ProductList({ products }) {
    const [search, setSearch] = useState("");
    const [showAvailableOnly, setShowAvailableOnly] = useState(false);
    const filteredProducts = useMemo(() => {
    return products.filter((product) => {
    const matchesSearch = product.name
    .toLowerCase()
    .includes(search.toLowerCase());
    const matchesAvailability =
    !showAvailableOnly || product.available;
    return matchesSearch && matchesAvailability;
    });
    }, [products, search, showAvailableOnly]);
    return (
    <div>
    <input
    value={search}
    onChange={(event) => setSearch(event.target.value)}
    />
    <label>
    <input
    type="checkbox"
    checked={showAvailableOnly}
    onChange={(event) =>
    setShowAvailableOnly(event.target.checked)
    }
    />
    Available only
    </label>
    <ul>
    {filteredProducts.map((product) => (
    <li key={product.id}>{product.name}</li>
    ))}
    </ul>
    </div>
    );
    }

    Important

    Do not add useMemo automatically just because filtering exists.

    For small lists, this is often perfectly fine:

    javascript
    const filteredProducts = products.filter(...);

    Use memoization when the calculation is sufficiently expensive or when stable derived values are useful for another optimization.

    Example: Stable Callback with `useCallback`

    Consider a memoized child component.

    javascript
    import { memo, useCallback, useState } from "react";
    const TodoItem = memo(function TodoItem({ todo, onToggle }) {
    console.log("Rendering:", todo.text);
    return (
    <li>
    <span>{todo.text}</span>
    <button onClick={() => onToggle(todo.id)}>
    Toggle
    </button>
    </li>
    );
    });
    function TodoList() {
    const [todos, setTodos] = useState([
    { id: 1, text: "Learn React", completed: false },
    { id: 2, text: "Build a project", completed: false },
    ]);
    const [theme, setTheme] = useState("light");
    const toggleTodo = useCallback((id) => {
    setTodos((currentTodos) =>
    currentTodos.map((todo) =>
    todo.id === id
    ? { ...todo, completed: !todo.completed }
    : todo
    )
    );
    }, []);
    return (
    <div className={theme}>
    <button
    onClick={() =>
    setTheme((current) =>
    current === "light" ? "dark" : "light"
    )
    }
    >
    Toggle Theme
    </button>
    <ul>
    {todos.map((todo) => (
    <TodoItem
    key={todo.id}
    todo={todo}
    onToggle={toggleTodo}
    />
    ))}
    </ul>
    </div>
    );
    }
    export default TodoList;

    The important relationship is:

    text
    useCallback
    |
    v
    stable function reference
    |
    v
    React.memo child
    |
    v
    can skip unnecessary rendering

    useCallback is not useful merely because a function exists.

    Example: Shopping Cart with Derived Totals

    A shopping cart demonstrates the difference between source state and derived values.

    javascript
    import { useState } from "react";
    function Cart() {
    const [items, setItems] = useState([
    { id: 1, name: "Keyboard", price: 2000, quantity: 2 },
    { id: 2, name: "Mouse", price: 1000, quantity: 1 },
    ]);
    const total = items.reduce(
    (sum, item) => sum + item.price * item.quantity,
    0
    );
    return (
    <div>
    {items.map((item) => (
    <div key={item.id}>
    {item.name} × {item.quantity}
    </div>
    ))}
    <strong>Total: ₹{total}</strong>
    </div>
    );
    }
    export default Cart;

    There is no need for:

    javascript
    const [total, setTotal] = useState(0);

    because:

    text
    items
    |
    v
    calculate total
    |
    v
    total

    The cart items are state.

    The total is derived data.

    Example: Complex Form with `useReducer`

    When many state transitions exist, a reducer can make the logic easier to reason about.

    javascript
    import { useReducer } from "react";
    const initialState = {
    name: "",
    email: "",
    status: "idle",
    error: null,
    };
    function formReducer(state, action) {
    switch (action.type) {
    case "fieldChanged":
    return {
    ...state,
    [action.field]: action.value,
    };
    case "submitted":
    return {
    ...state,
    status: "submitting",
    error: null,
    };
    case "success":
    return {
    ...state,
    status: "success",
    };
    case "failure":
    return {
    ...state,
    status: "error",
    error: action.message,
    };
    default:
    throw new Error(`Unknown action: ${action.type}`);
    }
    }
    function RegistrationForm() {
    const [state, dispatch] = useReducer(
    formReducer,
    initialState
    );
    function handleSubmit(event) {
    event.preventDefault();
    dispatch({ type: "submitted" });
    // API call would normally happen here or in a separate
    // async layer, not inside the reducer.
    }
    return (
    <form onSubmit={handleSubmit}>
    <input
    value={state.name}
    onChange={(event) =>
    dispatch({
    type: "fieldChanged",
    field: "name",
    value: event.target.value,
    })
    }
    />
    <input
    value={state.email}
    onChange={(event) =>
    dispatch({
    type: "fieldChanged",
    field: "email",
    value: event.target.value,
    })
    }
    />
    <button type="submit">Register</button>
    {state.status === "submitting" && <p>Submitting...</p>}
    {state.error && <p>{state.error}</p>}
    </form>
    );
    }
    export default RegistrationForm;

    The reducer remains pure:

    text
    action
    |
    v
    reducer
    |
    v
    new state

    It should not perform network requests, DOM operations, timers, or other side effects.

    Example: Reusable Local Storage Hook

    A Custom Hook can package repeated stateful behavior.

    javascript
    import { useEffect, useState } from "react";
    function useLocalStorage(key, initialValue) {
    const [value, setValue] = useState(() => {
    const storedValue = localStorage.getItem(key);
    if (storedValue === null) {
    return initialValue;
    }
    try {
    return JSON.parse(storedValue);
    } catch {
    return initialValue;
    }
    });
    useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
    }, [key, value]);
    return [value, setValue];
    }
    export default useLocalStorage;

    Now a component can use it:

    javascript
    import useLocalStorage from "./useLocalStorage";
    function ThemeSelector() {
    const [theme, setTheme] = useLocalStorage("theme", "light");
    return (
    <button
    onClick={() =>
    setTheme((current) =>
    current === "light" ? "dark" : "light"
    )
    }
    >
    Theme: {theme}
    </button>
    );
    }

    The component does not need to know how persistence works.

    It only knows the Hook's API:

    javascript
    const [theme, setTheme] = useLocalStorage("theme", "light");

    Example: Window Size Custom Hook

    Another useful example is tracking browser dimensions.

    javascript
    import { useEffect, useState } from "react";
    function useWindowSize() {
    const [size, setSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight,
    });
    useEffect(() => {
    function handleResize() {
    setSize({
    width: window.innerWidth,
    height: window.innerHeight,
    });
    }
    window.addEventListener("resize", handleResize);
    return () => {
    window.removeEventListener("resize", handleResize);
    };
    }, []);
    return size;
    }
    export default useWindowSize;

    Usage:

    javascript
    import useWindowSize from "./useWindowSize";
    function LayoutInfo() {
    const { width, height } = useWindowSize();
    return (
    <p>
    Window: {width} × {height}
    </p>
    );
    }

    The Hook owns the browser subscription and cleanup.

    The component only consumes the result.

    Example: Authentication State

    A common application pattern is separating authentication state from UI components.

    A simplified Custom Hook might look like:

    javascript
    import { useEffect, useState } from "react";
    function useCurrentUser() {
    const [user, setUser] = useState(null);
    const [status, setStatus] = useState("loading");
    useEffect(() => {
    async function loadUser() {
    try {
    const response = await fetch("/api/me");
    if (!response.ok) {
    setUser(null);
    setStatus("unauthenticated");
    return;
    }
    const data = await response.json();
    setUser(data);
    setStatus("authenticated");
    } catch {
    setStatus("error");
    }
    }
    loadUser();
    }, []);
    return {
    user,
    status,
    isAuthenticated: status === "authenticated",
    };
    }
    export default useCurrentUser;

    A component can then remain focused on presentation:

    javascript
    function Profile() {
    const {
    user,
    status,
    isAuthenticated,
    } = useCurrentUser();
    if (status === "loading") {
    return <p>Loading profile...</p>;
    }
    if (!isAuthenticated) {
    return <p>Please log in.</p>;
    }
    return <h1>Welcome, {user.name}</h1>;
    }

    The UI does not need to contain all authentication implementation details.

    Example: Modal with `useState` and `useEffect`

    A modal can combine state with an external browser effect.

    javascript
    import { useEffect, useState } from "react";
    function Modal({ onClose }) {
    useEffect(() => {
    function handleKeyDown(event) {
    if (event.key === "Escape") {
    onClose();
    }
    }
    window.addEventListener("keydown", handleKeyDown);
    return () => {
    window.removeEventListener("keydown", handleKeyDown);
    };
    }, [onClose]);
    return (
    <div role="dialog" aria-modal="true">
    <h2>Settings</h2>
    <button onClick={onClose}>Close</button>
    </div>
    );
    }
    function App() {
    const [open, setOpen] = useState(false);
    return (
    <div>
    <button onClick={() => setOpen(true)}>
    Open Settings
    </button>
    {open && <Modal onClose={() => setOpen(false)} />}
    </div>
    );
    }
    export default App;

    The Effect subscribes to a browser event while the modal exists and removes the subscription when the modal is removed.

    Example: Timer with Cleanup

    Timers are another classic Effect use case.

    javascript
    import { useEffect, useState } from "react";
    function Timer() {
    const [seconds, setSeconds] = useState(0);
    useEffect(() => {
    const intervalId = setInterval(() => {
    setSeconds((current) => current + 1);
    }, 1000);
    return () => {
    clearInterval(intervalId);
    };
    }, []);
    return <p>Elapsed: {seconds}s</p>;
    }
    export default Timer;

    The relationship is:

    text
    Component mounts
    |
    v
    setInterval()
    |
    v
    state updates every second
    |
    v
    Component unmounts
    |
    v
    clearInterval()

    Without cleanup, the timer could continue running after the component is no longer present.

    Example: Previous Value with `useRef`

    Refs can store information from a previous render without causing another render.

    javascript
    import { useEffect, useRef } from "react";
    function PreviousValue({ value }) {
    const previousValue = useRef(value);
    useEffect(() => {
    previousValue.current = value;
    }, [value]);
    return (
    <div>
    <p>Current: {value}</p>
    <p>Previous: {previousValue.current}</p>
    </div>
    );
    }
    export default PreviousValue;

    Conceptually:

    text
    Render
    |
    +--> read ref
    |
    v
    Effect
    |
    +--> store current value for future render

    The ref is not being used as UI state.

    It is simply retaining a mutable value between renders.

    Example: Combining Multiple Hooks

    A realistic component may use several Hooks together.

    javascript
    import {
    useCallback,
    useEffect,
    useMemo,
    useRef,
    useState,
    } from "react";
    function Dashboard({ products }) {
    const [search, setSearch] = useState("");
    const [darkMode, setDarkMode] = useState(false);
    const inputRef = useRef(null);
    const filteredProducts = useMemo(() => {
    return products.filter((product) =>
    product.name
    .toLowerCase()
    .includes(search.toLowerCase())
    );
    }, [products, search]);
    const clearSearch = useCallback(() => {
    setSearch("");
    inputRef.current?.focus();
    }, []);
    useEffect(() => {
    document.title = search
    ? `Searching: ${search}`
    : "Product Dashboard";
    return () => {
    document.title = "React App";
    };
    }, [search]);
    return (
    <div className={darkMode ? "dark" : "light"}>
    <button onClick={() => setDarkMode((current) => !current)}>
    Toggle theme
    </button>
    <input
    ref={inputRef}
    value={search}
    onChange={(event) => setSearch(event.target.value)}
    />
    <button onClick={clearSearch}>
    Clear
    </button>
    <p>{filteredProducts.length} products found</p>
    <ul>
    {filteredProducts.map((product) => (
    <li key={product.id}>{product.name}</li>
    ))}
    </ul>
    </div>
    );
    }
    export default Dashboard;

    Each Hook has a distinct responsibility:

    HookResponsibility
    useStateUI state
    useRefDOM reference
    useMemoExpensive/intentional derived calculation
    useCallbackStable callback identity
    useEffectSynchronization with the document

    This is the mindset to develop: Hooks should have clear responsibilities.

    Example: Avoiding an Unnecessary Effect

    Consider this:

    javascript
    function UserGreeting({ firstName, lastName }) {
    const [fullName, setFullName] = useState("");
    useEffect(() => {
    setFullName(`${firstName} ${lastName}`);
    }, [firstName, lastName]);
    return <h1>Hello, {fullName}</h1>;
    }

    This creates redundant state.

    Instead:

    javascript
    function UserGreeting({ firstName, lastName }) {
    const fullName = `${firstName} ${lastName}`;
    return <h1>Hello, {fullName}</h1>;
    }

    The second version is simpler.

    General rule

    If a value can be calculated directly from props or state during rendering, it usually does not need:

    • another state variable
    • an Effect
    • synchronization logic

    Example: Avoiding an Effect for an Event

    This pattern is often unnecessarily complicated:

    javascript
    function SaveButton() {
    const [shouldSave, setShouldSave] = useState(false);
    useEffect(() => {
    if (shouldSave) {
    saveDocument();
    }
    }, [shouldSave]);
    return (
    <button onClick={() => setShouldSave(true)}>
    Save
    </button>
    );
    }

    The event already tells you why the save should happen.

    Prefer:

    javascript
    function SaveButton() {
    function handleSave() {
    saveDocument();
    }
    return (
    <button onClick={handleSave}>
    Save
    </button>
    );
    }

    This makes the data flow easier to understand.

    Example: State vs Ref

    Suppose you want to track the number of button clicks and display it.

    Use state:

    javascript
    const [clicks, setClicks] = useState(0);

    If you want to store an interval ID:

    javascript
    const intervalRef = useRef(null);

    The difference is:

    text
    State
    -> changes should update UI
    -> causes re-render
    Ref
    -> value is needed across renders
    -> changing it does not cause re-render

    Choosing incorrectly can make a component harder to reason about.

    Example: Loading, Success, and Error States

    Real applications should explicitly model asynchronous states.

    javascript
    function UserList() {
    const [status, setStatus] = useState("idle");
    const [users, setUsers] = useState([]);
    const [error, setError] = useState(null);
    // ...
    }

    A useful state model is:

    text
    idle
    |
    v
    loading
    |
    +--> success
    |
    +--> error

    Then render based on the status:

    javascript
    if (status === "loading") {
    return <Spinner />;
    }
    if (status === "error") {
    return <ErrorMessage message={error} />;
    }
    return <UserList users={users} />;

    Explicit state models are easier to maintain than several unrelated boolean flags such as:

    javascript
    isLoading
    isLoaded
    hasError
    isEmpty
    isFetching

    which can accidentally represent contradictory combinations.

    Example: Hook Composition

    Custom Hooks become particularly powerful when they compose other Hooks.

    javascript
    function useOnlineStatus() {
    const [isOnline, setIsOnline] = useState(
    navigator.onLine
    );
    useEffect(() => {
    function handleOnline() {
    setIsOnline(true);
    }
    function handleOffline() {
    setIsOnline(false);
    }
    window.addEventListener("online", handleOnline);
    window.addEventListener("offline", handleOffline);
    return () => {
    window.removeEventListener("online", handleOnline);
    window.removeEventListener("offline", handleOffline);
    };
    }, []);
    return isOnline;
    }

    Usage:

    javascript
    function Checkout() {
    const isOnline = useOnlineStatus();
    if (!isOnline) {
    return <p>You are offline.</p>;
    }
    return <button>Place Order</button>;
    }

    The component does not manage browser events directly.

    The Custom Hook owns that responsibility.

    Example: Hook-Based Data Fetching Abstraction

    A reusable data Hook can expose a predictable API.

    javascript
    import { useEffect, useState } from "react";
    function useUsers() {
    const [state, setState] = useState({
    status: "loading",
    data: [],
    error: null,
    });
    useEffect(() => {
    const controller = new AbortController();
    async function loadUsers() {
    try {
    const response = await fetch("/api/users", {
    signal: controller.signal,
    });
    if (!response.ok) {
    throw new Error("Failed to fetch users");
    }
    const data = await response.json();
    setState({
    status: "success",
    data,
    error: null,
    });
    } catch (error) {
    if (error.name === "AbortError") {
    return;
    }
    setState({
    status: "error",
    data: [],
    error,
    });
    }
    }
    loadUsers();
    return () => controller.abort();
    }, []);
    return state;
    }

    A consumer can simply write:

    javascript
    function UsersPage() {
    const { status, data, error } = useUsers();
    if (status === "loading") {
    return <p>Loading...</p>;
    }
    if (status === "error") {
    return <p>{error.message}</p>;
    }
    return (
    <ul>
    {data.map((user) => (
    <li key={user.id}>{user.name}</li>
    ))}
    </ul>
    );
    }

    In larger applications, dedicated libraries for server-state management are often preferable to building a complete data-fetching system from scratch.

    A Realistic Hook Architecture

    A larger feature might look like this:

    text
    Page
    |
    +-- UI state
    | |
    | +-- useState
    | +-- useReducer
    |
    +-- reusable behavior
    | |
    | +-- useAuth
    | +-- useForm
    | +-- useDebounce
    | +-- useOnlineStatus
    |
    +-- external synchronization
    | |
    | +-- useEffect
    |
    +-- DOM interaction
    | |
    | +-- useRef
    |
    +-- performance optimization
    |
    +-- useMemo
    +-- useCallback

    The architecture is easier to maintain when each responsibility has a clear home.

    Hook Example: Debounce as a Custom Hook

    The debounce example can be extracted into reusable logic.

    javascript
    import { useEffect, useState } from "react";
    function useDebouncedValue(value, delay = 400) {
    const [debouncedValue, setDebouncedValue] = useState(value);
    useEffect(() => {
    const timer = setTimeout(() => {
    setDebouncedValue(value);
    }, delay);
    return () => {
    clearTimeout(timer);
    };
    }, [value, delay]);
    return debouncedValue;
    }
    export default useDebouncedValue;

    Now:

    javascript
    function SearchPage() {
    const [query, setQuery] = useState("");
    const debouncedQuery = useDebouncedValue(query, 500);
    // Use debouncedQuery for API searching.
    return (
    <input
    value={query}
    onChange={(event) => setQuery(event.target.value)}
    />
    );
    }

    This is a good example of abstraction because the implementation detail is reusable and the public API is simple.

    Hook Example: Form Field Abstraction

    A small form Hook might look like:

    javascript
    import { useState } from "react";
    function useFormField(initialValue = "") {
    const [value, setValue] = useState(initialValue);
    function onChange(event) {
    setValue(event.target.value);
    }
    function reset() {
    setValue(initialValue);
    }
    return {
    value,
    onChange,
    reset,
    setValue,
    };
    }

    Usage:

    javascript
    function ProfileForm() {
    const name = useFormField("");
    const email = useFormField("");
    return (
    <form>
    <input
    value={name.value}
    onChange={name.onChange}
    placeholder="Name"
    />
    <input
    value={email.value}
    onChange={email.onChange}
    placeholder="Email"
    />
    <button type="button" onClick={name.reset}>
    Reset Name
    </button>
    </form>
    );
    }

    A Custom Hook should make repeated logic easier to use, not hide important behavior unnecessarily.

    Common Real-World Mistakes

    Mistake 1: State for derived values

    Avoid:

    javascript
    const [total, setTotal] = useState(0);

    when total can be calculated from existing state.

    Mistake 2: Effect for every calculation

    Avoid:

    javascript
    useEffect(() => {
    setFullName(firstName + lastName);
    }, [firstName, lastName]);

    when the value can be derived during rendering.

    Mistake 3: useMemo everywhere

    Avoid:

    javascript
    const name = useMemo(() => user.name, [user.name]);

    There is normally no meaningful performance benefit here.

    Mistake 4: useCallback everywhere

    Avoid wrapping every function:

    javascript
    const handleClick = useCallback(() => {
    console.log("clicked");
    }, []);

    Use it when function identity matters to an optimization or another dependency relationship.

    Mistake 5: Ref instead of state

    Avoid using:

    javascript
    const valueRef = useRef(0);

    for information that must appear in the UI.

    Use state when the UI should update.

    Mistake 6: Side effects inside reducers

    Never put network requests or DOM manipulation inside a reducer.

    Reducers should calculate the next state from:

    text
    previous state + action

    Mistake 7: Ignoring cleanup

    Subscriptions, timers, event listeners, and other external resources often require cleanup.

    Mistake 8: Race conditions in asynchronous work

    If multiple requests can overlap, an older response can arrive after a newer response.

    Use appropriate cancellation or ignore/staleness handling.

    Real-World Hook Combination Checklist

    When building a feature, ask:

    State

    • What information actually changes over time?
    • Does the UI need to re-render when it changes?
    • Can some values be derived instead?

    Effects

    • Am I synchronizing with something outside React?
    • Does the Effect have the correct dependencies?
    • Does it need cleanup?
    • Can asynchronous work become stale?

    Refs

    • Do I need a DOM node?
    • Do I need mutable information that should survive renders without causing renders?

    Memoization

    • Is there a measured performance reason?
    • Is the calculation actually expensive?
    • Does stable identity matter?

    Custom Hooks

    • Is this stateful logic repeated?
    • Can the behavior have a clean reusable API?
    • Is the abstraction hiding complexity rather than adding it?

    Mini Project: Searchable Product Dashboard

    Build a small product dashboard using multiple Hooks.

    Requirements

    The application should:

    1. Display a list of products.
    2. Provide a search field.
    3. Filter products by name.
    4. Provide an availability filter.
    5. Show a loading state for simulated asynchronous data.
    6. Provide a clear-search button.
    7. Focus the search input when cleared.
    8. Display the number of matching products.
    9. Persist the selected theme.
    10. Keep reusable logic inside Custom Hooks where appropriate.

    Suggested Hook usage

    text
    ProductDashboard
    |
    +-- useState
    | +-- search
    | +-- products
    |
    +-- useEffect
    | +-- load products
    |
    +-- useMemo
    | +-- filtered products
    |
    +-- useRef
    | +-- search input
    |
    +-- useCallback
    | +-- clear search
    |
    +-- Custom Hook
    +-- useLocalStorage for theme

    Challenge

    Do not add a Hook simply because the project contains several Hooks.

    Every Hook should have a clear responsibility.

    Practice Exercises

    Exercise 1: Counter

    Create a counter with:

    • increment
    • decrement
    • reset
    • increment by 5

    Use functional state updates where appropriate.

    Exercise 2: Todo List

    Create a todo application with:

    • add todo
    • toggle completed
    • delete todo
    • filter all/active/completed

    Do not store the filtered list as separate state.

    Exercise 3: Search

    Create a search component that:

    • accepts a query
    • debounces the query
    • fetches results
    • cancels outdated requests
    • displays loading and error states

    Exercise 4: Custom Hook

    Create:

    javascript
    useToggle(initialValue)

    It should return:

    javascript
    const [value, toggle] = useToggle(false);

    Exercise 5: Previous Value

    Create a component that displays:

    text
    Current value: 10
    Previous value: 8

    Use useRef.

    Exercise 6: Timer

    Create a timer with:

    • start
    • pause
    • reset

    Make sure intervals are cleaned up correctly.

    Exercise 7: Theme

    Create a theme selector that:

    • switches between light and dark
    • persists the theme
    • restores the theme when the page reloads

    Extract persistence into a Custom Hook.

    Interview questions & answers

    Interview preparation

    20 questions grouped by difficulty — expand any card for a model answer and follow-up probe.

    Beginner

    5
    1. 1When should you use useState?
      Beginner

      Model answer

      Use useState when a component owns UI values that change over time and should trigger a re-render, such as form fields, toggles, or local list filters.
    2. 2What is the difference between state and derived data?
      Beginner

      Model answer

      State is stored information that changes independently. Derived data is calculated from existing props or state during render—for example filtering a product list from a search term—so it usually should not be duplicated in another useState.
    3. 3What problem does useEffect solve?
      Beginner

      Model answer

      useEffect synchronizes a component with an external system after render, such as fetching data, timers, subscriptions, or updating the document title—not ordinary calculations that belong in render.
    4. 4What is useRef used for?
      Beginner

      Model answer

      useRef holds a persistent mutable value across renders without causing updates. Common uses are DOM node references, previous values, timers IDs, and third-party library instances.
    5. 5Why does changing a ref not cause a re-render?
      Beginner

      Model answer

      Updating ref.current mutates a box React keeps for you; it is not state. React only re-renders when state or props change, so ref mutations alone do not schedule a render.

    Intermediate

    8
    1. 6When should you use a functional state update?
      Intermediate

      Model answer

      Use the functional updater when the next state depends on the previous state—for example incrementing a counter from its prior value—especially with batched or repeated updates where reading the current variable can be stale.

    2. 7Why should Effects usually synchronize with external systems?
      Intermediate

      Model answer

      Effects exist to keep React in sync with something outside React.

      Using them for derived values or event responses adds unnecessary timing complexity; calculate during render or handle events directly instead.

    3. 8Why is cleanup important?
      Intermediate

      Model answer

      Cleanup cancels or undoes work the Effect started—clearing timers, aborting fetches, or removing listeners—so stale updates and leaks do not happen when dependencies change or the component unmounts.

    4. 9When should you use useMemo?
      Intermediate

      Model answer

      Use useMemo when a calculation is expensive enough to skip on most renders, or when you need a stable derived reference for another optimization. Do not wrap every filter or map by default.
    5. 10When should you use useCallback?
      Intermediate

      Model answer

      Use useCallback when a stable function identity matters—typically a callback passed to a React.memo child or listed in another Hook's dependency array—not merely because a function exists.
    6. 11What makes a Custom Hook useful?
      Intermediate

      Model answer

      A Custom Hook extracts reusable React-specific behavior (state, Effects, refs) behind a clear use-prefixed API so multiple components share logic without sharing the same state instance.
    7. 12Why should reducers remain pure?
      Intermediate

      Model answer

      Pure reducers receive state and an action and return the next state without side effects or mutation. That keeps transitions predictable, testable, and easy to reason about inside useReducer.
    8. 13How can asynchronous Effects create race conditions?
      Intermediate

      Model answer

      If a request finishes after a newer dependency change or unmount, it can still call setState with stale data. Cleanup must ignore or abort outdated responses so only the latest relevant result updates state.

    Advanced

    7
    1. 14How would you design a reusable data-fetching Hook?
      Advanced

      Model answer

      Expose status, data, error, and actions; keep the fetch inside an Effect with dependency-driven re-runs; own cleanup for cancellation; and hide service details so UI components only consume a small Hook API.

    2. 15How would you prevent stale network responses from updating state?
      Advanced

      Model answer

      Track a request id or use AbortController in Effect cleanup. Ignore responses that no longer match the current query, or abort the previous request whenever dependencies change.
    3. 16How do React.memo and useCallback work together?
      Advanced

      Model answer

      React.memo skips re-rendering a child when props are referentially equal. useCallback keeps a parent callback stable so the memoized child is not forced to re-render by a new function identity every parent render.
    4. 17When can memoization make performance worse?
      Advanced

      Model answer

      Memoization adds dependency tracking and memory overhead. If the work is cheap or dependencies change almost every render, useMemo / useCallback can cost more than they save.
    5. 18How would you decide whether logic belongs in a component or Custom Hook?
      Advanced

      Model answer

      Keep one-off UI wiring in the component. Extract a Custom Hook when the behavior is reusable, substantial, or mixes several Hooks into a named capability such as useDebouncedValue or useLocalStorage.
    6. 19How would you model complex asynchronous UI states?
      Advanced

      Model answer

      Model explicit statuses such as idle, loading, success, and error—often with useReducer or a small state machine—so the UI can render each path clearly instead of juggling many loosely related booleans.
    7. 20How would you separate server state from local UI state?
      Advanced

      Model answer

      Keep remote data (lists, details, cache) in a fetch/data layer or server-state library, and keep ephemeral UI concerns (modals, draft inputs, selected tabs) in local useState / feature Hooks.

    Quick Reference

    ProblemTypical solution
    UI value changesuseState
    Complex state transitionsuseReducer
    Synchronize with external systemuseEffect
    DOM referenceuseRef
    Mutable value without renderuseRef
    Expensive calculationuseMemo
    Stable callback identityuseCallback
    Reusable stateful behaviorCustom Hook
    Derived valueCalculate during render
    Timer/subscriptionuseEffect + cleanup
    Browser event subscriptionuseEffect + cleanup
    Focus DOM elementuseRef
    Persistent browser storageCustom Hook + Effect
    Ready to mark this lesson complete?Track your journey across the entire course.