Hook Debugging
Hook debugging covers stale closures, dependency mistakes, infinite effect loops, DevTools techniques, and systematic ways to diagnose Hook bugs.
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:
Props / State↓Render↓JSX↓Commit↓Effects↓External systems
A state update can start the process again:
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:
"The component is broken."
identify:
"The API request runs three times."
or:
"The callback uses the previous count."
or:
"The modal closes and immediately opens again."
Then ask:
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:
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:
function Counter({ initialValue }) {const [count, setCount] =useState(initialValue);return <p>{count}</p>;}
If the displayed value is wrong, inspect:
initialValuecount
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:
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:
setCount(count + 1);
requests a future render.
It does not mutate the current render's count variable.
Debugging Stale State
Suppose:
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:
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?
setCount(count + 1);
Should the update use previous state?
If yes:
setCount((current) => current + 1);
Is the state being mutated?
Bad:
user.name = "Asha";
Good:
setUser((current) => ({...current,name: "Asha",}));
Is the state actually needed?
If it can be derived:
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:
- When does the Effect run?
- Which dependencies does it have?
- Which dependencies changed?
- Does it have cleanup?
- Does it update state?
- Does that state update cause the Effect to run again?
Example:
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.
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:
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:
function Component() {const [count, setCount] = useState(0);useEffect(() => {setCount(count + 1);}, [count]);return <p>{count}</p>;}
The sequence is:
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:
Dependency↓Effect↓State update↓Dependency
If the graph forms a cycle:
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:
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:
const fullName = `${firstName} ${lastName}`;
This removes an entire source of potential debugging problems.
Effect Runs Too Often
Suppose:
const options = {roomId,theme,};useEffect(() => {connect(options);}, [options]);
The options object is recreated during every render.
Therefore:
Render 1 → options ARender 2 → options BRender 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:
useEffect(() => {connect({roomId,theme,});}, [roomId, theme]);
This often removes unnecessary object identity changes.
Unstable Function Dependencies
Consider:
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
useEffect(() => {async function loadUser() {const user = await fetchUser(userId);// ...}loadUser();}, [userId]);
Or use useCallback when a stable function reference is genuinely useful
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:
const filters = {category,minPrice,};useEffect(() => {search(filters);}, [filters]);
If filters is recreated each render, the dependency changes.
Possible solution:
useEffect(() => {search({category,minPrice,});}, [category, minPrice]);
The same issue can occur with arrays:
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:
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:
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:
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:
const countRef = useRef(count);useEffect(() => {countRef.current = count;}, [count]);
Then another long-lived callback can read:
countRef.current
Use this pattern only when it matches the actual synchronization requirement.
Debugging useRef
Refs do not cause renders when .current changes.
Example:
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:
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:
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:
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:
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:
setCount(count + 1);
use:
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:
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:
useEffect(() => {window.addEventListener("resize",handleResize);return () => {window.removeEventListener("resize",handleResize);};}, []);
The function passed to removal must correspond to the listener that was registered.
Avoid:
window.addEventListener("resize",() => handleResize());window.removeEventListener("resize",() => handleResize());
Those are different function references.
Debugging Timers
Incorrect:
useEffect(() => {setInterval(() => {console.log("tick");}, 1000);}, []);
There is no cleanup.
Correct:
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:
URLmethodparametersrequest countrequest timingresponsecleanupcomponent 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:
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:
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:
setupcleanupsetup
This is intentional development behavior designed to expose unsafe Effect logic.
Do not immediately "fix" it by adding flags such as:
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:
useEffect(() => {const connection =createConnection(roomId);connection.connect();return () => {connection.disconnect();};}, [roomId]);
The relationship is:
Setup↓connectCleanup↓disconnect
This symmetry makes Effects easier to reason about.
Debugging Re-Renders
A re-render is not automatically a bug.
Ask:
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:
function Parent() {const [count, setCount] = useState(0);return (<><buttononClick={() =>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:
const Child = React.memo(function Child({onClick,}) {return <button onClick={onClick}>Save</button>;});
The child may still render when the parent renders if:
const onClick = () => {// ...};
is recreated.
If stable identity matters:
const onClick = useCallback(() => {// ...}, []);
But remember:
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:
<AuthContext.Providervalue={{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:
<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:
{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:
key={Math.random()}
This can cause items to be treated as new components.
Avoid index keys when list order can change:
key={index}
Prefer stable identifiers:
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:
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:
if (enabled) {useEffect(() => {});}
Correct:
useEffect(() => {if (!enabled) {return;}// logic}, [enabled]);
Debugging useReducer
When reducer state is wrong, inspect:
Previous state↓Action↓Reducer↓Next state
Temporarily log:
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:
const result = useMemo(() => expensiveCalculation(data),[data]);
Ask:
- Is
datachanging 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:
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:
function Component() {console.count("Component render");// ...}
This can help answer:
How often is this component rendering?
You can also log specific values:
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:
setState(...)
event handlers:
function handleSubmit() {// breakpoint}
Effects:
useEffect(() => {// breakpoint}, []);
reducers:
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:
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
.currentbeing 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:
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:
function Search({ query }) {const [results, setResults] =useState([]);useEffect(() => {setResults(expensiveSearch(query));}, [query, results]);return (<Results results={results} />);}
Why can this loop?
query/results↓Effect↓setResults↓results changes↓Effect↓setResults
The Effect does not need results to perform the search.
Correct:
useEffect(() => {setResults(expensiveSearch(query));}, [query]);
Or, if the search is simply a pure calculation:
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:
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:
const logCount = useCallback(() => {console.log(count);}, [count]);
If stable identity is not important, a normal function may be enough:
function logCount() {console.log(count);}
Do not use useCallback merely to solve a stale-value problem.
Practical Debugging Example: State Update
Problem:
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:
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:
useEffect(() => {window.addEventListener("resize",handleResize);}, []);
If the component is mounted and unmounted repeatedly, listeners can accumulate.
Correct:
useEffect(() => {window.addEventListener("resize",handleResize);return () => {window.removeEventListener("resize",handleResize);};}, []);
Practical Debugging Example: Unexpected State Reset
Suppose:
<Item key={Math.random()} />
Each render creates a new key.
React may treat the item as a new component:
Old Item↓removedNew Item↓mounted
Its state can therefore reset.
Use a stable key:
<Item key={item.id} />
Practical Debugging Example: Effect Runs Every Render
Problem:
const options = {sort,filter,};useEffect(() => {updateResults(options);}, [options]);
The object is recreated on every render.
A simpler approach is often:
useEffect(() => {updateResults({sort,filter,});}, [sort, filter]);
This makes the actual dependencies explicit.
Practice Exercises
Exercise 1: Find the Infinite Loop
Identify the problem:
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:
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:
const filtered = useMemo(() => {return products.filter((product) =>product.category === category);}, [products]);
Exercise 4: Fix the Effect Cleanup
Find and fix the resource leak:
useEffect(() => {const timer = setInterval(refreshData,5000);}, []);
Exercise 5: Debug an Unstable Dependency
Explain why this Effect can run on every render:
const options = {query,page,};useEffect(() => {fetchData(options);}, [options]);
Refactor it.
Exercise 6: Debug State Reset
Explain why this is problematic:
items.map((item) => (<Itemkey={Math.random()}item={item}/>));
Replace it with a stable key.
Exercise 7: Debug a Stale Callback
Fix:
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:
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
1Why can a React component render more than once?
BeginnerModel answer
Possible causes include:- state updates
- parent renders
- changed props
- context updates
- external subscriptions
- development behavior such as Strict Mode
2Why does an Effect run repeatedly?
BeginnerModel 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
3Why is cleanup important?
BeginnerModel answer
Cleanup stops external resources created by an Effect. Examples:event listeners → timers → subscriptions → WebSockets → observers → external instances → requests4Why can object dependencies cause Effects to run repeatedly?
BeginnerModel answer
Objects are compared by reference.
A new object created on each render has a different reference even when its contents are identical.
Intermediate
5What is a stale closure?
IntermediateModel answer
A stale closure occurs when a callback or Effect uses values captured from an earlier render.
6Should you remove dependencies to stop an Effect from running?
IntermediateModel 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.
7How do you debug an infinite Effect loop?
IntermediateModel answer
Trace:dependency → Effect → state update → dependencyFind the feedback cycle and determine whether the Effect should exist, whether the state is necessary, or whether the synchronization design should change.
Advanced
8How can you fix stale state updates?
AdvancedModel 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.9Why might state reset unexpectedly?
AdvancedModel answer
The component may have been remounted due to:- changing keys
- changed component identity
- conditional structure
- moving components in the tree
10What is the difference between a re-render and a remount?
AdvancedModel 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
console.log("state", state);
Inspect props
console.log("props", props);
Count renders
console.count("render");
Inspect Effect
useEffect(() => {console.log("Effect ran");}, [value]);
Functional update
setCount((current) => current + 1);
Cleanup
useEffect(() => {const timer = setInterval(callback,1000);return () => {clearInterval(timer);};}, []);
Stable key
key={item.id}
Derived value
const total = price * quantity;
Effect dependency
useEffect(() => {synchronize(value);}, [value]);
Debugging flow
Symptom↓Reproduce↓Inspect↓Trace↓Find first unexpected behavior↓Fix cause↓Verify