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

    Hook Debugging

    Hook debugging covers stale closures, dependency mistakes, infinite effect loops, DevTools techniques, and systematic ways to diagnose Hook bugs.

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

    React Hooks make component logic powerful and composable, but Hook-related bugs can sometimes be difficult to understand.

    Common problems include:

    • Effects running more often than expected
    • stale values inside Effects or callbacks
    • infinite Effect loops
    • incorrect dependency arrays
    • unnecessary re-renders
    • state updates using stale values
    • incorrect cleanup
    • Hooks being called in invalid locations
    • unstable object and function references
    • confusing development behavior caused by Strict Mode

    Good Hook debugging starts with understanding rendering, state snapshots, dependencies, references, and Effect lifecycle.

    A React Debugging Mental Model

    When a component behaves unexpectedly, think through this sequence:

    text
    Props / State
    ↓
    Render
    ↓
    JSX
    ↓
    Commit
    ↓
    Effects
    ↓
    External systems

    A state update can start the process again:

    text
    State update
    ↓
    New render
    ↓
    New JSX
    ↓
    Commit
    ↓
    Effect synchronization

    Debugging becomes easier when you identify which stage is actually producing the unexpected behavior.

    Start With the Symptom

    Before changing code, describe the problem precisely.

    Instead of:

    text
    "The component is broken."

    identify:

    text
    "The API request runs three times."

    or:

    text
    "The callback uses the previous count."

    or:

    text
    "The modal closes and immediately opens again."

    Then ask:

    text
    What changed?
    What rendered?
    What Effect ran?
    What dependency changed?
    What state update occurred?

    Precise symptoms lead to better debugging.

    React DevTools

    React DevTools is one of the most useful tools for debugging React applications.

    It can help you inspect:

    • component hierarchy
    • props
    • state
    • context
    • component renders
    • performance
    • component relationships

    A typical debugging workflow is:

    text
    Reproduce problem
    ↓
    Open React DevTools
    ↓
    Find affected component
    ↓
    Inspect props/state
    ↓
    Check component updates
    ↓
    Trace the source of the change

    Use DevTools to understand the actual component tree instead of guessing.

    Inspecting Props and State

    Suppose:

    javascript
    function Counter({ initialValue }) {
    const [count, setCount] =
    useState(initialValue);
    return <p>{count}</p>;
    }

    If the displayed value is wrong, inspect:

    text
    initialValue
    count

    Ask:

    • Did the prop contain the expected value?
    • Was the state initialized as expected?
    • Was state updated later?
    • Is the component being remounted?

    This is usually more effective than immediately changing the Hook code.

    State Is a Snapshot

    One of the most important debugging concepts is that state behaves like a snapshot for a particular render.

    Example:

    javascript
    function Counter() {
    const [count, setCount] = useState(0);
    function handleClick() {
    console.log(count);
    setCount(count + 1);
    console.log(count);
    }
    return (
    <button onClick={handleClick}>
    {count}
    </button>
    );
    }

    Both logs inside the same event handler can show the value from the current render.

    Calling:

    javascript
    setCount(count + 1);

    requests a future render.

    It does not mutate the current render's count variable.

    Debugging Stale State

    Suppose:

    javascript
    function handleClick() {
    setCount(count + 1);
    setCount(count + 1);
    }

    Both updates may use the same count value from the current render.

    If you need to apply multiple updates based on the previous state, use a functional update:

    javascript
    function handleClick() {
    setCount((current) => current + 1);
    setCount((current) => current + 1);
    }

    Now each update receives the latest pending state.

    This is especially useful when debugging counters, queues, or sequential state changes.

    Debugging State Updates

    When state is wrong, ask:

    What value did the update read?

    javascript
    setCount(count + 1);

    Should the update use previous state?

    If yes:

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

    Is the state being mutated?

    Bad:

    javascript
    user.name = "Asha";

    Good:

    javascript
    setUser((current) => ({
    ...current,
    name: "Asha",
    }));

    Is the state actually needed?

    If it can be derived:

    javascript
    const fullName = `${firstName} ${lastName}`;

    do not create redundant state just to synchronize it.

    Debugging useEffect

    Effects are a common source of confusion.

    Start by asking:

    1. When does the Effect run?
    2. Which dependencies does it have?
    3. Which dependencies changed?
    4. Does it have cleanup?
    5. Does it update state?
    6. Does that state update cause the Effect to run again?

    Example:

    javascript
    useEffect(() => {
    console.log("Effect ran");
    }, [query]);

    The important question is not simply:

    Why did useEffect run?

    Ask:

    What caused one of its dependencies to change?

    Log Effect Dependencies

    A useful debugging technique is to log the values used by an Effect.

    javascript
    useEffect(() => {
    console.log("Effect:", {
    query,
    userId,
    filter,
    });
    }, [query, userId, filter]);

    If the Effect runs unexpectedly, inspect which value changed.

    You can also log before the Effect:

    javascript
    console.log({
    query,
    userId,
    filter,
    });

    Compare the values across renders.

    Infinite Effect Loops

    One of the most common Hook bugs is an Effect that repeatedly causes itself to run.

    Example:

    javascript
    function Component() {
    const [count, setCount] = useState(0);
    useEffect(() => {
    setCount(count + 1);
    }, [count]);
    return <p>{count}</p>;
    }

    The sequence is:

    text
    count changes
    ↓
    Effect runs
    ↓
    setCount()
    ↓
    count changes
    ↓
    Effect runs
    ↓
    setCount()
    ↓
    ...

    This creates a feedback loop.

    The solution is not simply to remove count from the dependency array.

    First ask:

    Why does the Effect need to update this state?

    Often the Effect should be removed or redesigned.

    Debugging Effect Feedback Loops

    When an Effect loops, draw the dependency relationship:

    text
    Dependency
    ↓
    Effect
    ↓
    State update
    ↓
    Dependency

    If the graph forms a cycle:

    text
    A → Effect → B → A

    you may have an Effect feedback loop.

    Possible solutions:

    • remove unnecessary state
    • derive the value during rendering
    • move user-triggered logic into an event handler
    • synchronize with the correct external system
    • restructure state ownership

    Unnecessary Effect

    Consider:

    javascript
    const [fullName, setFullName] = useState("");
    useEffect(() => {
    setFullName(`${firstName} ${lastName}`);
    }, [firstName, lastName]);

    The Effect is not synchronizing with an external system.

    The value is derived.

    Replace it with:

    javascript
    const fullName = `${firstName} ${lastName}`;

    This removes an entire source of potential debugging problems.

    Effect Runs Too Often

    Suppose:

    javascript
    const options = {
    roomId,
    theme,
    };
    useEffect(() => {
    connect(options);
    }, [options]);

    The options object is recreated during every render.

    Therefore:

    text
    Render 1 → options A
    Render 2 → options B
    Render 3 → options C

    Even if the contents are the same, the object references differ.

    The Effect can therefore run more often than intended.

    One solution is to depend on the primitive values directly:

    javascript
    useEffect(() => {
    connect({
    roomId,
    theme,
    });
    }, [roomId, theme]);

    This often removes unnecessary object identity changes.

    Unstable Function Dependencies

    Consider:

    javascript
    function Component({ userId }) {
    const getUser = () => {
    return fetchUser(userId);
    };
    useEffect(() => {
    getUser();
    }, [getUser]);
    return <div />;
    }

    The function is recreated on every render.

    Therefore the dependency changes.

    Depending on the actual logic, possible solutions include:

    Move the function inside the Effect

    javascript
    useEffect(() => {
    async function loadUser() {
    const user = await fetchUser(userId);
    // ...
    }
    loadUser();
    }, [userId]);

    Or use useCallback when a stable function reference is genuinely useful

    javascript
    const getUser = useCallback(() => {
    return fetchUser(userId);
    }, [userId]);

    Do not add useCallback automatically. First consider whether the function can simply live inside the Effect or be structured differently.

    Object and Array Dependencies

    References matter.

    Example:

    javascript
    const filters = {
    category,
    minPrice,
    };
    useEffect(() => {
    search(filters);
    }, [filters]);

    If filters is recreated each render, the dependency changes.

    Possible solution:

    javascript
    useEffect(() => {
    search({
    category,
    minPrice,
    });
    }, [category, minPrice]);

    The same issue can occur with arrays:

    javascript
    const ids = products.map(
    (product) => product.id
    );
    useEffect(() => {
    loadProducts(ids);
    }, [ids]);

    If the array is recreated on every render, the Effect may run repeatedly.

    Whether to use useMemo or restructure the logic depends on the actual cost and dependency relationship.

    Dependency Arrays Are Not Performance Switches

    Avoid this:

    javascript
    useEffect(() => {
    // ...
    // intentionally omit dependency
    }, []);

    just because you want the Effect to run less often.

    The dependency list should reflect the Effect's reactive dependencies.

    If the dependency relationship is difficult, restructure the Effect.

    Stale Closures

    A stale closure occurs when a callback uses values captured from an older render.

    Example:

    javascript
    function Counter() {
    const [count, setCount] = useState(0);
    useEffect(() => {
    const timer = setInterval(() => {
    console.log(count);
    }, 1000);
    return () => clearInterval(timer);
    }, []);
    return <p>{count}</p>;
    }

    The callback captures the count value from the render that created the Effect.

    If the Effect needs to respond to changing count, its dependency relationship must reflect that.

    For example:

    javascript
    useEffect(() => {
    const timer = setInterval(() => {
    console.log(count);
    }, 1000);
    return () => clearInterval(timer);
    }, [count]);

    But this recreates the interval whenever count changes.

    Whether that is correct depends on the desired behavior.

    Sometimes a ref is more appropriate for reading the latest mutable value without recreating the subscription:

    javascript
    const countRef = useRef(count);
    useEffect(() => {
    countRef.current = count;
    }, [count]);

    Then another long-lived callback can read:

    javascript
    countRef.current

    Use this pattern only when it matches the actual synchronization requirement.

    Debugging useRef

    Refs do not cause renders when .current changes.

    Example:

    javascript
    const valueRef = useRef(0);
    valueRef.current += 1;

    If the UI does not update, that may be expected.

    Ask:

    Does this value affect what the user should see?

    If yes, state may be appropriate.

    If no, a ref may be appropriate.

    Debugging useMemo

    If a memoized calculation appears stale, inspect its dependencies.

    Example:

    javascript
    const filtered = useMemo(() => {
    return products.filter(
    (product) =>
    product.category === category
    );
    }, [products]);

    The calculation uses category, but the dependency list does not include it.

    This can produce an outdated result.

    Correct:

    javascript
    const filtered = useMemo(() => {
    return products.filter(
    (product) =>
    product.category === category
    );
    }, [products, category]);

    A missing dependency is a correctness problem, not merely a performance issue.

    Debugging useCallback

    Suppose:

    javascript
    const handleSave = useCallback(() => {
    save(user);
    }, []);

    The callback uses user but does not list it as a dependency.

    This can cause the callback to use stale data.

    Correct:

    javascript
    const handleSave = useCallback(() => {
    save(user);
    }, [user]);

    Or restructure the callback if possible.

    Functional State Updates

    Functional updates are useful when the next state depends on the previous state.

    Instead of:

    javascript
    setCount(count + 1);

    use:

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

    This is especially useful for:

    • intervals
    • event queues
    • multiple updates
    • asynchronous callbacks
    • state updates where current closure values may be stale

    Example:

    javascript
    useEffect(() => {
    const timer = setInterval(() => {
    setCount((current) => current + 1);
    }, 1000);
    return () => clearInterval(timer);
    }, []);

    The Effect does not need to recreate the interval simply because count changes.

    Debugging Cleanup

    If you see:

    • duplicate event handling
    • multiple WebSocket messages
    • timers firing after unmount
    • increasing memory usage
    • repeated subscriptions

    inspect the Effect cleanup.

    Example:

    javascript
    useEffect(() => {
    window.addEventListener(
    "resize",
    handleResize
    );
    return () => {
    window.removeEventListener(
    "resize",
    handleResize
    );
    };
    }, []);

    The function passed to removal must correspond to the listener that was registered.

    Avoid:

    javascript
    window.addEventListener(
    "resize",
    () => handleResize()
    );
    window.removeEventListener(
    "resize",
    () => handleResize()
    );

    Those are different function references.

    Debugging Timers

    Incorrect:

    javascript
    useEffect(() => {
    setInterval(() => {
    console.log("tick");
    }, 1000);
    }, []);

    There is no cleanup.

    Correct:

    javascript
    useEffect(() => {
    const timer = setInterval(() => {
    console.log("tick");
    }, 1000);
    return () => {
    clearInterval(timer);
    };
    }, []);

    If the timer should depend on a value, include that value appropriately or use a functional/ref-based strategy when a long-lived timer is required.

    Debugging API Requests

    When an API request behaves unexpectedly, inspect:

    text
    URL
    method
    parameters
    request count
    request timing
    response
    cleanup
    component lifecycle

    Questions to ask:

    • Is the Effect running more often than expected?
    • Is the dependency array correct?
    • Is a request being cancelled?
    • Can requests overlap?
    • Can stale results overwrite newer results?
    • Is the component being remounted?
    • Is Strict Mode exposing an unsafe Effect?

    Race Conditions

    Suppose:

    text
    Search A starts
    ↓
    Search B starts
    ↓
    B finishes
    ↓
    A finishes

    If A updates state after B, old data can overwrite new data.

    A request can be cancelled using AbortController:

    javascript
    useEffect(() => {
    const controller =
    new AbortController();
    async function search() {
    try {
    const response = await fetch(
    `/api/search?q=${encodeURIComponent(query)}`,
    {
    signal: controller.signal,
    }
    );
    const data = await response.json();
    setResults(data);
    } catch (error) {
    if (error.name !== "AbortError") {
    setError(error);
    }
    }
    }
    search();
    return () => {
    controller.abort();
    };
    }, [query]);

    Now changing query cleans up the previous request.

    Debugging Strict Mode

    Strict Mode can make Effects appear to run twice during development.

    For example, you may observe:

    text
    setup
    cleanup
    setup

    This is intentional development behavior designed to expose unsafe Effect logic.

    Do not immediately "fix" it by adding flags such as:

    javascript
    const hasRun = useRef(false);
    if (hasRun.current) {
    return;
    }
    hasRun.current = true;

    Such guards can hide lifecycle problems rather than solving them.

    Instead, make setup and cleanup correctly mirror each other.

    Setup/Cleanup Symmetry

    Good:

    javascript
    useEffect(() => {
    const connection =
    createConnection(roomId);
    connection.connect();
    return () => {
    connection.disconnect();
    };
    }, [roomId]);

    The relationship is:

    text
    Setup
    ↓
    connect
    Cleanup
    ↓
    disconnect

    This symmetry makes Effects easier to reason about.

    Debugging Re-Renders

    A re-render is not automatically a bug.

    Ask:

    text
    Why did this component render?

    Possible causes include:

    • its state changed
    • its parent rendered
    • its props changed
    • a consumed context value changed
    • an external subscription triggered an update

    Use React DevTools Profiler when you need detailed render information.

    Parent Re-Renders

    Suppose:

    javascript
    function Parent() {
    const [count, setCount] = useState(0);
    return (
    <>
    <button
    onClick={() =>
    setCount((value) => value + 1)
    }
    >
    {count}
    </button>
    <Child />
    </>
    );
    }

    When count changes, Parent renders again.

    The child may also render depending on the component structure and optimization.

    Do not assume that every child render is harmful.

    First determine whether the child performs expensive work.

    Debugging React.memo

    Suppose:

    javascript
    const Child = React.memo(function Child({
    onClick,
    }) {
    return <button onClick={onClick}>Save</button>;
    });

    The child may still render when the parent renders if:

    javascript
    const onClick = () => {
    // ...
    };

    is recreated.

    If stable identity matters:

    javascript
    const onClick = useCallback(() => {
    // ...
    }, []);

    But remember:

    text
    React.memo + useCallback

    is useful only when avoiding the child render actually matters.

    Debugging Context

    Context consumers can re-render when the provided context value changes.

    Suppose:

    javascript
    <AuthContext.Provider
    value={{
    user,
    login,
    logout,
    }}
    >

    If this object is recreated frequently, consumers may observe a changed context value.

    If profiling identifies context churn as a real issue, you can consider stable callbacks and memoized context values.

    But do not optimize Context automatically.

    Component Remounting vs Re-rendering

    These are different.

    Re-render

    The existing component instance renders again.

    State is generally preserved.

    Remount

    The old component instance is removed and a new one is created.

    State is reset.

    For example, changing a key can cause remounting:

    javascript
    <Component key={someValue} />

    If someValue changes, React may treat it as a different component identity.

    This can explain why state unexpectedly resets.

    Debugging State Resets

    If state suddenly returns to its initial value, investigate:

    • component keys
    • conditional component types
    • component position in the tree
    • conditional rendering
    • remounting
    • parent architecture

    For example:

    javascript
    {isEditing ? (
    <Editor />
    ) : (
    <Editor />
    )}

    Although the JSX looks similar, changing component structure or keys can affect identity.

    Stable component identity is important for preserving state.

    Debugging Keys

    Avoid unstable keys such as:

    javascript
    key={Math.random()}

    This can cause items to be treated as new components.

    Avoid index keys when list order can change:

    javascript
    key={index}

    Prefer stable identifiers:

    javascript
    key={item.id}

    Incorrect keys can cause:

    • state moving between list items
    • components remounting
    • unexpected input values
    • unnecessary DOM work

    Debugging Custom Hooks

    When a Custom Hook behaves unexpectedly, inspect it as if it were a component-level abstraction.

    Example:

    javascript
    function useSearch(query) {
    const [results, setResults] =
    useState([]);
    useEffect(() => {
    // ...
    }, [query]);
    return results;
    }

    Ask:

    • What state does the Hook own?
    • What dependencies does it use?
    • What external systems does it synchronize with?
    • Does cleanup occur?
    • Does it return stable references when necessary?
    • Does each consumer have independent state?

    Remember that each invocation has its own Hook state.

    Debugging Hook Rules

    If you see a Hook-order error, inspect whether Hooks are called:

    • conditionally
    • inside loops
    • inside nested functions
    • inside event handlers
    • inside callbacks
    • after conditional returns

    Incorrect:

    javascript
    if (enabled) {
    useEffect(() => {});
    }

    Correct:

    javascript
    useEffect(() => {
    if (!enabled) {
    return;
    }
    // logic
    }, [enabled]);

    Debugging useReducer

    When reducer state is wrong, inspect:

    text
    Previous state
    ↓
    Action
    ↓
    Reducer
    ↓
    Next state

    Temporarily log:

    javascript
    function reducer(state, action) {
    console.log({
    state,
    action,
    });
    switch (action.type) {
    // ...
    default:
    return state;
    }
    }

    Check:

    • action type
    • action payload
    • state shape
    • immutable updates
    • default case
    • unexpected actions

    Because reducers are pure functions, they are often straightforward to test independently.

    Debugging useMemo

    If useMemo seems ineffective:

    javascript
    const result = useMemo(
    () => expensiveCalculation(data),
    [data]
    );

    Ask:

    • Is data changing every render?
    • Is the calculation actually expensive?
    • Is the component rendering frequently?
    • Does the memoization save meaningful work?

    If data is always a new object, the memoized calculation will be reconsidered.

    Debugging useCallback

    If useCallback seems ineffective:

    javascript
    const handleClick = useCallback(
    () => doSomething(value),
    [value]
    );

    Ask:

    • Is a dependency changing every render?
    • Is the callback passed to a memoized child?
    • Does the child actually perform expensive rendering?
    • Does the stable reference matter?

    If none of these matter, the useCallback may simply be unnecessary.

    Logging Render Counts

    A simple development technique is:

    javascript
    function Component() {
    console.count("Component render");
    // ...
    }

    This can help answer:

    How often is this component rendering?

    You can also log specific values:

    javascript
    console.log("render", {
    user,
    query,
    loading,
    });

    Use these logs temporarily and remove noisy debugging code after the issue is understood.

    Debugging With Breakpoints

    Browser developer tools allow you to pause JavaScript execution.

    Useful places for breakpoints include:

    javascript
    setState(...)

    event handlers:

    javascript
    function handleSubmit() {
    // breakpoint
    }

    Effects:

    javascript
    useEffect(() => {
    // breakpoint
    }, []);

    reducers:

    javascript
    function reducer(state, action) {
    // breakpoint
    }

    This allows you to inspect actual runtime values rather than relying only on logs.

    A Systematic Debugging Process

    Use this workflow:

    Step 1: Reproduce

    Find the smallest sequence that causes the problem.

    Step 2: Describe

    Write down the exact symptom.

    Step 3: Inspect

    Check props, state, and component identity.

    Step 4: Trace

    Follow:

    text
    Event
    ↓
    State update
    ↓
    Render
    ↓
    Effect
    ↓
    External system

    Step 5: Identify the First Unexpected Step

    Do not focus on the final error first.

    Find where behavior first becomes incorrect.

    Step 6: Fix the Cause

    Avoid patches that merely hide the symptom.

    Step 7: Re-test

    Verify the original problem and related behavior.

    Debugging Checklist

    When a Hook behaves unexpectedly:

    State

    • Is the initial state correct?
    • Is the update using the latest state?
    • Should this use a functional update?
    • Is state being mutated?

    Effects

    • What dependencies does the Effect use?
    • Which dependency changed?
    • Does the Effect update state?
    • Is there a feedback loop?
    • Does it need cleanup?

    Memoization

    • Are dependencies complete?
    • Are dependencies stable?
    • Is memoization actually necessary?

    Callbacks

    • Is the callback using stale values?
    • Does function identity matter?
    • Are dependencies correct?

    Refs

    • Should this value trigger a render?
    • Is .current being used appropriately?

    Components

    • Is the component rendering or remounting?
    • Are keys stable?
    • Is state being preserved?

    Async Work

    • Can requests overlap?
    • Can stale results overwrite newer results?
    • Is cancellation needed?

    Development

    • Is Strict Mode exposing a lifecycle problem?

    Common Debugging Mistakes

    1. Removing Dependencies to Stop an Effect

    This can hide the real problem and introduce stale data.

    2. Adding useMemo Everywhere

    Memoization does not automatically solve rendering problems.

    3. Adding useCallback Everywhere

    Stable references matter only in certain situations.

    4. Ignoring Component Remounting

    Unexpected state resets are often identity or key problems rather than state-management problems.

    5. Treating Strict Mode as a Bug

    Strict Mode may expose Effects that are not correctly implemented.

    6. Mutating State During Debugging

    Temporary mutations can make behavior even harder to reason about.

    7. Fixing Symptoms Instead of Causes

    For example:

    javascript
    if (hasRun.current) {
    return;
    }

    may hide an Effect lifecycle issue instead of fixing it.

    8. Debugging Without Reproduction

    Make the problem reproducible before making many code changes.

    Best Practices

    1. Understand the Render Cycle

    Know when rendering, commits, and Effects occur.

    2. Treat State as a Snapshot

    Do not expect a state setter to mutate the current render's variable.

    3. Keep Effects Focused

    Each Effect should represent a coherent synchronization concern.

    4. Keep Dependencies Correct

    Do not use dependency arrays as arbitrary execution controls.

    5. Prefer Functional Updates When Appropriate

    Especially when updating based on previous state.

    6. Keep Setup and Cleanup Symmetrical

    Every resource created by an Effect should have an appropriate cleanup strategy.

    7. Measure Before Optimizing

    Use profiling when performance is the actual problem.

    8. Use Stable Keys

    Stable identity prevents many list-state bugs.

    9. Extract Reusable Logic

    Custom Hooks can make complicated Hook behavior easier to isolate and test.

    10. Use Linting

    React Hook lint rules can catch many dependency and Hook-order mistakes early.

    Practical Debugging Example: Infinite Loop

    Problem:

    javascript
    function Search({ query }) {
    const [results, setResults] =
    useState([]);
    useEffect(() => {
    setResults(
    expensiveSearch(query)
    );
    }, [query, results]);
    return (
    <Results results={results} />
    );
    }

    Why can this loop?

    text
    query/results
    ↓
    Effect
    ↓
    setResults
    ↓
    results changes
    ↓
    Effect
    ↓
    setResults

    The Effect does not need results to perform the search.

    Correct:

    javascript
    useEffect(() => {
    setResults(
    expensiveSearch(query)
    );
    }, [query]);

    Or, if the search is simply a pure calculation:

    javascript
    const results = useMemo(() => {
    return expensiveSearch(query);
    }, [query]);

    The correct solution depends on whether the operation is a pure calculation or an external synchronization.

    Practical Debugging Example: Stale Callback

    Problem:

    javascript
    function Counter() {
    const [count, setCount] =
    useState(0);
    const logCount = useCallback(() => {
    console.log(count);
    }, []);
    return (
    <button onClick={logCount}>
    {count}
    </button>
    );
    }

    The callback captures the initial count.

    If the callback should always use the latest count:

    javascript
    const logCount = useCallback(() => {
    console.log(count);
    }, [count]);

    If stable identity is not important, a normal function may be enough:

    javascript
    function logCount() {
    console.log(count);
    }

    Do not use useCallback merely to solve a stale-value problem.

    Practical Debugging Example: State Update

    Problem:

    javascript
    function Counter() {
    const [count, setCount] =
    useState(0);
    function handleClick() {
    setCount(count + 1);
    setCount(count + 1);
    }
    return (
    <button onClick={handleClick}>
    {count}
    </button>
    );
    }

    If the intention is to increment twice:

    javascript
    function handleClick() {
    setCount((current) => current + 1);
    setCount((current) => current + 1);
    }

    The functional form makes each update depend on the latest pending state.

    Practical Debugging Example: Missing Cleanup

    Problem:

    javascript
    useEffect(() => {
    window.addEventListener(
    "resize",
    handleResize
    );
    }, []);

    If the component is mounted and unmounted repeatedly, listeners can accumulate.

    Correct:

    javascript
    useEffect(() => {
    window.addEventListener(
    "resize",
    handleResize
    );
    return () => {
    window.removeEventListener(
    "resize",
    handleResize
    );
    };
    }, []);

    Practical Debugging Example: Unexpected State Reset

    Suppose:

    javascript
    <Item key={Math.random()} />

    Each render creates a new key.

    React may treat the item as a new component:

    text
    Old Item
    ↓
    removed
    New Item
    ↓
    mounted

    Its state can therefore reset.

    Use a stable key:

    javascript
    <Item key={item.id} />

    Practical Debugging Example: Effect Runs Every Render

    Problem:

    javascript
    const options = {
    sort,
    filter,
    };
    useEffect(() => {
    updateResults(options);
    }, [options]);

    The object is recreated on every render.

    A simpler approach is often:

    javascript
    useEffect(() => {
    updateResults({
    sort,
    filter,
    });
    }, [sort, filter]);

    This makes the actual dependencies explicit.

    Practice Exercises

    Exercise 1: Find the Infinite Loop

    Identify the problem:

    javascript
    const [count, setCount] = useState(0);
    useEffect(() => {
    setCount(count + 1);
    }, [count]);

    Explain the update cycle and propose a correct design.

    Exercise 2: Fix the Stale State

    Fix:

    javascript
    function handleClick() {
    setCount(count + 1);
    setCount(count + 1);
    }

    so that one click increments the count twice.

    Exercise 3: Fix the Missing Dependency

    Identify the missing dependency:

    javascript
    const filtered = useMemo(() => {
    return products.filter(
    (product) =>
    product.category === category
    );
    }, [products]);

    Exercise 4: Fix the Effect Cleanup

    Find and fix the resource leak:

    javascript
    useEffect(() => {
    const timer = setInterval(
    refreshData,
    5000
    );
    }, []);

    Exercise 5: Debug an Unstable Dependency

    Explain why this Effect can run on every render:

    javascript
    const options = {
    query,
    page,
    };
    useEffect(() => {
    fetchData(options);
    }, [options]);

    Refactor it.

    Exercise 6: Debug State Reset

    Explain why this is problematic:

    javascript
    items.map((item) => (
    <Item
    key={Math.random()}
    item={item}
    />
    ));

    Replace it with a stable key.

    Exercise 7: Debug a Stale Callback

    Fix:

    javascript
    const save = useCallback(() => {
    submit(user);
    }, []);

    Explain why the original can use stale data.

    Mini Project: Hook Debugging Lab

    Create a small React application containing several intentionally introduced Hook bugs.

    Include:

    Bug 1

    An Effect with an infinite update loop.

    Bug 2

    A stale callback.

    Bug 3

    A missing cleanup function.

    Bug 4

    An unstable object dependency.

    Bug 5

    A state update using a stale snapshot.

    Bug 6

    An unstable list key.

    Bug 7

    An unnecessary Effect for derived data.

    Then debug each issue using:

    • React DevTools
    • console logging
    • browser breakpoints
    • dependency inspection
    • render tracing
    • Effect lifecycle reasoning

    For every bug, document:

    text
    Symptom
    ↓
    Cause
    ↓
    Evidence
    ↓
    Fix
    ↓
    Why the fix works

    This turns debugging into a repeatable engineering process rather than trial and error.

    Interview questions & answers

    Interview preparation

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

    Beginner

    4
    1. 1Why can a React component render more than once?
      Beginner

      Model answer

      Possible causes include:
      • state updates
      • parent renders
      • changed props
      • context updates
      • external subscriptions
      • development behavior such as Strict Mode
      A re-render is not automatically a bug.
    2. 2Why does an Effect run repeatedly?
      Beginner

      Model answer

      Possible causes include:
      • a dependency changes
      • an object or function dependency is recreated
      • the Effect updates state that changes a dependency
      • the component remounts
      • development Strict Mode behavior
    3. 3Why is cleanup important?
      Beginner

      Model answer

      Cleanup stops external resources created by an Effect. Examples: event listeners → timers → subscriptions → WebSockets → observers → external instances → requests
    4. 4Why can object dependencies cause Effects to run repeatedly?
      Beginner

      Model answer

      Objects are compared by reference.

      A new object created on each render has a different reference even when its contents are identical.

    Intermediate

    3
    1. 5What is a stale closure?
      Intermediate

      Model answer

      A stale closure occurs when a callback or Effect uses values captured from an earlier render.

    2. 6Should you remove dependencies to stop an Effect from running?
      Intermediate

      Model answer

      No.

      Dependencies describe the Effect's reactive inputs.

      If the Effect is incorrect or runs too often, fix the underlying dependency or synchronization design.

    3. 7How do you debug an infinite Effect loop?
      Intermediate

      Model answer

      Trace: dependency → Effect → state update → dependency Find the feedback cycle and determine whether the Effect should exist, whether the state is necessary, or whether the synchronization design should change.

    Advanced

    3
    1. 8How can you fix stale state updates?
      Advanced

      Model answer

      Use functional updates when the next state depends on the previous state: setCount((current) => current + 1); Other cases may require correct dependencies, refs, or restructuring.
    2. 9Why might state reset unexpectedly?
      Advanced

      Model answer

      The component may have been remounted due to:
      • changing keys
      • changed component identity
      • conditional structure
      • moving components in the tree
    3. 10What is the difference between a re-render and a remount?
      Advanced

      Model answer

      A re-render updates an existing component instance.

      A remount removes the old instance and creates a new one, so state is initialized again.

    Debugging Cheat Sheet

    Inspect state

    javascript
    console.log("state", state);

    Inspect props

    javascript
    console.log("props", props);

    Count renders

    javascript
    console.count("render");

    Inspect Effect

    javascript
    useEffect(() => {
    console.log("Effect ran");
    }, [value]);

    Functional update

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

    Cleanup

    javascript
    useEffect(() => {
    const timer = setInterval(
    callback,
    1000
    );
    return () => {
    clearInterval(timer);
    };
    }, []);

    Stable key

    javascript
    key={item.id}

    Derived value

    javascript
    const total = price * quantity;

    Effect dependency

    javascript
    useEffect(() => {
    synchronize(value);
    }, [value]);

    Debugging flow

    text
    Symptom
    ↓
    Reproduce
    ↓
    Inspect
    ↓
    Trace
    ↓
    Find first unexpected behavior
    ↓
    Fix cause
    ↓
    Verify

    Key Takeaways

    Ready to mark this lesson complete?Track your journey across the entire course.