Diagnosing React Components That Don’t Re‑Render After a State Change
A step‑by‑step guide to identify why a React UI stays unchanged after calling a state setter, with common causes, checks, fixes, and when to escalate.
17 Apr 2026, 14:11 UTC

Recognizable Condition
The user interacts with the UI (e.g., clicks a button, types in an input) and a state setter is called, but the visible output does not change. React DevTools shows the state value updated, yet the component’s render output remains the same.
Cause / Diagnostic Table
| Possible Cause | What You Might See | Quick Check |
|---|---|---|
| State immutability violated (mutating object/array in place) | State appears updated in DevTools but UI unchanged | Inspect the setter call: are you doing stateObj.prop = newValue or arr.push(item)? |
Setter invoked with same reference (e.g., setCount(count)) | State value looks identical before and after the call | Log prev === next inside the setter or use setCount(c => c + 1) |
| useEffect with empty dependency array closes over stale state | Effect runs only once; later state changes don’t trigger the effect | Check the effect’s dependency array; does it reference any state or props? |
| Memoized component receives unchanged props because memoization key never changes | Component wrapped in React.memo or using useMemo never re‑renders despite prop change | Verify that the props used as the memo key actually differ between renders |
| Component’s key changes on every render (forcing unmount/remount) | State is lost each render; UI seems stuck | Look at the key prop on the component or its children; is it based on index or a volatile value? |
| Error thrown in render or lifecycle stops the update cycle | Console shows an error; UI freezes at the point of failure | Open the console; any red error during the state update? |
| Update batched behind a promise that resolves after component unmounts | State setter called inside an async callback, but component removed before callback resolves | Check if the setter is inside a Promise.then, setTimeout, or similar async without cleanup |
| Custom hook returns stale value due to missing dependencies | Hook’s returned value never updates even though its sources change | Review the hook’s internal useEffect or useMemo dependency arrays |
Ordered Checks
- Verify the state setter call. Add a temporary
console.logbefore and after the setter to confirm the new value differs from the previous one. - Inspect for direct mutations. Search the code for
.push,.splice,obj.prop =, or any assignment to state properties. - Check the setter’s argument. If you’re passing the current state variable directly (
setCount(count)), switch to the functional form (setCount(c => c + 1)). - Review useEffect dependencies. Ensure every variable referenced inside the effect appears in its dependency array.
- Examine memoization keys. For
React.memocomponents, confirm that the props used for equality check actually change; foruseMemo, verify the dependency array. - Validate component keys. If you’re rendering a list, make sure the
keyprop uses a stable identifier (e.g., an ID) rather than the array index or a random value. - Look for runtime errors. Open the browser console; any error thrown during render will halt further updates.
- Trace async updates. If the setter is inside a
Promise,setTimeout, or event handler, confirm the component is still mounted when the callback runs (consider using a cleanup flag oruseRef). - Audit custom hooks. Check that any external values used inside the hook are listed in its dependency arrays.
Fixes Tied to Findings
- State immutability: Replace mutating operations with immutable updates, e.g.,
setItems([...items, newItem])orsetObj({ ...obj, prop: newValue }). - Same‑reference setter: Use the functional updater form when the new state depends on the previous state:
setCount(prev => prev + 1). - Stale closure in useEffect: Add missing dependencies to the effect’s array, or use a
useRefto hold the latest value if you intentionally want to ignore changes. - Memoization key not changing: Ensure the props passed to the memoized component actually vary; if they shouldn’t, reconsider whether memoization is needed.
- Changing key prop: Replace volatile keys (
index,Date.now()) with a stable ID from your data (item.id). - Render‑time error: Fix the thrown error (e.g., null‑check, proper try/catch) so React can continue the update cycle.
- Async update after unmount: Track mount status with a ref (
const mounted = useRef(true)) and clear it in a cleanup function; only call the setter ifmounted.currentis true. - Stale custom hook: Add any missing external dependencies to the hook’s internal
useEffectoruseMemoarrays.
Example: Fixing a Stale Closure in useEffect
Consider a component that fetches data based on a prop userId but the effect never re‑runs when the prop changes:
function UserProfile({ userId }) {
const [data, setData] = useState(null);
useEffect(() => {
// This closure captures the initial userId only
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(setData);
}, []); // ← missing dependency
return {data ? data.name : 'Loading…'};
}
The fix is to add userId to the dependency array:
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(setData);
}, [userId]); // ← now re‑runs when userId changes
After the change, DevTools will show the effect re‑executing on each userId update, and the UI will reflect the new data.
Limitations and Practical Verification
This guide covers the most frequent reasons a React component appears to ignore state updates, but it does not exhaust every edge case (e.g., concurrent mode batching nuances or third‑party library interference). To verify that a fix worked:
- Open React DevTools and select the component.
- Observe the
PropsandStatepanels while triggering the update. - Confirm that the state value changes and that the component’s
Renderhighlight appears (indicating a re‑render). - If the component is wrapped in
React.memo, ensure the props diff istruebefore the render.
If after applying the relevant fix the UI still does not update, proceed to the escalation criteria below.
Escalation Criteria
Move to deeper investigation when:
- Console shows warnings like “Maximum update depth exceeded” or “useEffect has missing dependencies” that persist after correcting dependencies.
- The component is part of a concurrent feature (e.g.,
startTransition) and the update is intentionally deferred; verify withuseTransitionreturn values. - State updates originate from a third‑party state manager (e.g., Redux, Zustand) and the issue may lie in middleware or selector immutability.
- You suspect a bug in the React version itself; test with a newer or older stable release to see if the behavior changes.
In these cases, create a minimal reproducible example (e.g., in CodeSandbox) and consider filing a bug report with the relevant library or React team.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.