This article walks through several React state patterns and shows when each one causes a re-render.
Plain useState Work as expected
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function App ( ) { const [count, setCount] = useState (0 ) useEffect (() => { const intervalId = setInterval (() => { setCount (count => count + 1 ) }, 1000 ) return () => clearInterval (intervalId) }, []) return ( <div > {count} </div > ) }
Plain variable NO change
Mutating a local let does not trigger a re-render, so the UI keeps showing 0.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function App ( ) { let count = 0 useEffect (() => { const intervalId = setInterval (() => { count += 1 }, 1000 ) return () => clearInterval (intervalId) }, [count]) return ( <div > {count} </div > ) }
Pass state to child Work as expected
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 function Component ({ count }: { count: number } ) { return ( <div > {count} </div > ) } function App ( ) { const [count, setCount] = useState (0 ) setInterval (() => { setCount (count + 1 ) }, 1000 ) return ( <div > <Component count ={count} /> </div > ) }
Pass variable to child NO change
Parent never re-renders, so the child keeps receiving the initial 0.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 function Component ({ count }: { count: number } ) { return ( <div > {count} </div > ) } function App ( ) { let count = 0 useEffect (() => { const intervalId = setInterval (() => { count += 1 }, 1000 ) return () => clearInterval (intervalId) }, [count]) return ( <div > <Component count ={count} /> </div > ) }
State in hook Work as expected
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 const useCount = ( ) => { const [count, setCount] = useState (0 ) return { count, setCount } } function App ( ) { const { count, setCount } = useCount () useEffect (() => { const intervalId = setInterval (() => { setCount (count => count + 1 ) }, 1000 ) return () => clearInterval (intervalId) }, [setCount]) return ( <div > {count} </div > ) }
Change state in hook Work as expected
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 const useCount = ( ) => { const [count, setCount] = useState (0 ) useEffect (() => { const intervalId = setInterval (() => { setCount (count => count + 1 ) }, 1000 ) return () => clearInterval (intervalId) }, []) return { count } } function App ( ) { const { count } = useCount () return ( <div > {count} </div > ) }
Plain ref NO change
Updating ref.current does not trigger a re-render, so JSX still shows the first read.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function App ( ) { const countRef = useRef (0 ) useEffect (() => { const intervalId = setInterval (() => { countRef.current += 1 }, 1000 ) return () => clearInterval (intervalId) }, []) return ( <div > {countRef.current} </div > ) }
Ref in memo NO change
useMemo(..., []) caches the initial 0; ref updates never recompute it.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 function App ( ) { const countRef = useRef (0 ) useEffect (() => { const intervalId = setInterval (() => { countRef.current += 1 }, 1000 ) return () => clearInterval (intervalId) }, []) const count = useMemo (() => countRef.current , []) return ( <div > {count} </div > ) }
Passing variable to child with memo NO change
Parent never re-renders, so useMemo in the child never sees a new count.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 function Component ({ count }: { count: number } ) { const localCount = useMemo (() => count, [count]) return ( <div > {localCount} </div > ) } function App ( ) { let count = 0 useEffect (() => { const intervalId = setInterval (() => { count += 1 }, 1000 ) return () => clearInterval (intervalId) }, []) return ( <div > <Component count ={count} /> </div > ) }
Passing states in a hook through another hook Work as expected
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 const useCount = ( ) => { const [count, setCount] = useState (0 ) useEffect (() => { const intervalId = setInterval (() => { setCount (count => count + 1 ) }, 1000 ) return () => clearInterval (intervalId) }, []) return { count } } const useCount2 = (count2: number ) => { return { count2 } } function App ( ) { const { count } = useCount () const { count2 } = useCount2 (count) return ( <div > {count2} </div > ) }
Hook inside hook Work as expected
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 const useCount = ( ) => { const [count, setCount] = useState (0 ) useEffect (() => { const intervalId = setInterval (() => { setCount (count => count + 1 ) }, 1000 ) return () => clearInterval (intervalId) }, []) return { count, setCount } } const useCount2 = ( ) => { const { count : count2 } = useCount () return { count2 } } function App ( ) { const { count2 } = useCount2 () return ( <div > {count2} </div > ) }
Change child only Not change in parent
Child state updates re-render only the child; the parent’s local count stays 0.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 function Component ( ) { const [localCount, setLocalCount] = useState (0 ) useEffect (() => { const intervalId = setInterval (() => { setLocalCount (localCount => localCount + 1 ) }, 1000 ) return () => clearInterval (intervalId) }, []) return ( <div > {localCount} </div > ) } function App ( ) { let count = 0 useEffect (() => { const intervalId = setInterval (() => { count += 1 }, 1000 ) return () => clearInterval (intervalId) }, []) return ( <div > {count} <Component /> </div > ) }
Ref within hook and pass out as returned value NO Change
Returning countRef.current snapshots 0 each render; mutating the ref does not re-render.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 const useCount = ( ) => { const countRef = useRef (0 ) useEffect (() => { const intervalId = setInterval (() => { countRef.current += 1 }, 1000 ) return () => clearInterval (intervalId) }, []) return { count : countRef.current } } function App ( ) { const { count } = useCount () return ( <div > {count} </div > ) }
Seed state from ref Work as expected
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 const useCount = (ref: React.RefObject<number> ) => { const [count, setCount] = useState (ref.current ) useEffect (() => { const intervalId = setInterval (() => { setCount (count => count + 1 ) }, 1000 ) return () => clearInterval (intervalId) }, []) return { count } } function App ( ) { const countRef = useRef (10 ) const { count } = useCount (countRef) return ( <div > {count} </div > ) }
Use ref together with state Work as expected
ref mutations become visible only when something else triggers a render.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 function App ( ) { const countRef = useRef (0 ) const [count, setCount] = useState (0 ) useEffect (() => { const intervalId = setInterval (() => { countRef.current += 1 }, 1000 ) return () => clearInterval (intervalId) }, []) useEffect (() => { const intervalId = setInterval (() => { setCount (count => count + 1 ) }, 1000 ) return () => clearInterval (intervalId) }, []) return ( <div > {count} <br /> {countRef.current} </div > ) }
Use ref to trigger useEffect No change
countRef.current in the dep array is only read on render; mutating the ref does not re-run the effect (may bump once on mount).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 function App ( ) { const countRef = useRef (0 ) const [count, setCount] = useState (0 ) useEffect (() => { const intervalId = setInterval (() => { countRef.current += 1 }, 1000 ) return () => clearInterval (intervalId) }, []) useEffect (() => { setCount (count => count + 1 ) }, [countRef.current ]) return ( <div > {count} </div > ) }
Use variable to trigger useEffect No change
Mutating countValue does not re-render, so the dep never changes after mount.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 function App ( ) { let countValue = 0 const [count, setCount] = useState (0 ) useEffect (() => { const intervalId = setInterval (() => { countValue += 1 }, 1000 ) return () => clearInterval (intervalId) }, []) useEffect (() => { setCount (count => count + 1 ) }, [countValue]) return ( <div > {count} </div > ) }
Change variable that is dependency of useEffect Not work
Clicking mutates count without setState, so React does not re-render or re-run the effect.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 function App ( ) { const countRef = useRef (0 ) let count = 0 const [changed, setChanged] = useState (false ) useEffect (() => { if (countRef.current !== count) { setChanged (true ) } }, [count]) return ( <div > <button onClick ={() => count += 1}>Increment</button > <br /> {changed ? 'changed' : 'not changed'} </div > ) }
Let useEffect dependent on a variable NO change
Module-level mutation is invisible to React; [countValue] stays the mount-time snapshot.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 let countValue = 0 function App ( ) { const [count, setCount] = useState (0 ) useEffect (() => { const intervalId = setInterval (() => { countValue += 1 }, 1000 ) return () => clearInterval (intervalId) }, []) useEffect (() => { setCount (countValue) }, [countValue]) return ( <div > {count} </div > ) }
Object as state but update value in object NO change
Mutating then setState with the same object reference bails out (Object.is).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 function App ( ) { const [countObject, setCountObject] = useState ({ count : 0 }) useEffect (() => { const intervalId = setInterval (() => { countObject.count += 1 setCountObject (countObject) }, 1000 ) return () => clearInterval (intervalId) }, [countObject]) return ( <div > {countObject.count} </div > ) }
Object as state but update object Work as expected
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function App ( ) { const [countObject, setCountObject] = useState ({ count : 0 }) useEffect (() => { const intervalId = setInterval (() => { setCountObject (countObject => ({ ...countObject, count : countObject.count + 1 })) }, 1000 ) return () => clearInterval (intervalId) }, [countObject]) return ( <div > {countObject.count} </div > ) }
Parent changes state while child changes variables NO change in child
Child re-renders with parent but let count = 0 resets each render, so the display stays 0.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 function Component ( ) { let count = 0 useEffect (() => { const intervalId = setInterval (() => { count++ }, 1000 ) return () => clearInterval (intervalId) }, [count]) return ( <div > {count} </div > ) } function App ( ) { const [count, setCount] = useState (0 ) useEffect (() => { const intervalId = setInterval (() => { setCount (count => count + 1 ) }, 1000 ) return () => clearInterval (intervalId) }, []) return ( <div > <Component /> {count} </div > ) }
Parent changes state and pass the state to the child Work as expected
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 function Component ({ count }: { count: number } ) { return ( <div > {count} </div > ) } function App ( ) { const [count, setCount] = useState (0 ) useEffect (() => { const intervalId = setInterval (() => { setCount (count => count + 1 ) }, 1000 ) return () => clearInterval (intervalId) }, []) return ( <div > <Component count ={count} /> </div > ) }
Parent changes state and pass a variable to the child NO change
The child component actually updated but the value is not passed in.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 function Component ({ count }: { count: number } ) { return ( <div > {count} </div > ) } function App ( ) { const [count, setCount] = useState (0 ) let count2 = 0 useEffect (() => { const intervalId = setInterval (() => { setCount (count => count + 1 ) count2 += 1 }, 1000 ) return () => clearInterval (intervalId) }, []) return ( <div > <Component count ={count2} /> </div > ) }
Parent changes state while child has memo with unchanged parameter NO change as expected, and logs are not printed every seconds
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 const Component = memo (({ count }: { count: number } ) => { console .log ('count' , count) return ( <div > {count} </div > ) }) function App ( ) { const [count, setCount] = useState (0 ) useEffect (() => { const intervalId = setInterval (() => { setCount (count => count + 1 ) }, 1000 ) return () => clearInterval (intervalId) }, []) return ( <div > <Component count ={0} /> </div > ) }
With react context and state updates Work as expected
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 const ComponentContext = createContext<number>(0 )const ChildComponent = ( ) => { const count = useContext (ComponentContext ) return ( <div > {count} </div > ) } function App ( ) { const [count, setCount] = useState (0 ) useEffect (() => { const intervalId = setInterval (() => { setCount (count => count + 1 ) }, 1000 ) return () => clearInterval (intervalId) }, []) return ( <div > <ComponentContext.Provider value ={count} > <ChildComponent /> </ComponentContext.Provider > </div > ) }
Functional updater Work as expected
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function App ( ) { const [count, setCount] = useState (0 ) useEffect (() => { const intervalId = setInterval (() => { setCount (count => count + 1 ) }, 1000 ) return () => clearInterval (intervalId) }, []) return ( <div > {count} </div > ) }
setState bail-out on same value NO re-render (bail-out)
console.log runs on the first render only; later setCount(0) is ignored because the value is already 0.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 function App ( ) { const [count, setCount] = useState (0 ) console .log ('render' , count) useEffect (() => { const intervalId = setInterval (() => { setCount (0 ) }, 1000 ) return () => clearInterval (intervalId) }, []) return ( <div > {count} </div > ) }
memo defeated by unstable props Re-renders (memo defeated)
Parent re-renders every second and passes a new inline function each time, so memo cannot skip the child.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 const Component = memo (({ onTick }: { onTick: () => void } ) => { console .log ('child render' ) return ( <div > <button onClick ={onTick} > tick</button > </div > ) }) function App ( ) { const [count, setCount] = useState (0 ) useEffect (() => { const intervalId = setInterval (() => { setCount (count => count + 1 ) }, 1000 ) return () => clearInterval (intervalId) }, []) return ( <div > {count} <Component onTick ={() => {}} /> </div > ) }
External store with useSyncExternalStore Work as expected
Mutating a module-level value alone does not re-render; notifying listeners via useSyncExternalStore does. subscribe registers the component, getSnapshot returns the current value, and calling listeners after storeCount += 1 schedules a re-render with the new snapshot.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 let storeCount = 0 const listeners = new Set <() => void >()const subscribe = (listener: () => void ) => { listeners.add (listener) return () => listeners.delete (listener) } const getSnapshot = ( ) => storeCountconst getServerSnapshot = ( ) => 0 function increment ( ) { storeCount += 1 listeners.forEach ((listener ) => listener ()) } function CountDisplay ( ) { const count = useSyncExternalStore (subscribe, getSnapshot, getServerSnapshot) return <p > Display: {count}</p > } function CountControls ( ) { const count = useSyncExternalStore (subscribe, getSnapshot, getServerSnapshot) return ( <div > <p > Controls: {count}</p > <button onClick ={increment} > +</button > </div > ) } function App ( ) { useEffect (() => { const id = setInterval (increment, 1000 ) return () => clearInterval (id) }, []) return ( <div > <CountDisplay /> <CountControls /> </div > ) }
Remount via key State resets on key change
Clicking the button changes key, remounts Component, and local count goes back to 0.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 function Component ( ) { const [localCount, setLocalCount] = useState (0 ) useEffect (() => { const intervalId = setInterval (() => { setLocalCount (localCount => localCount + 1 ) }, 1000 ) return () => clearInterval (intervalId) }, []) return ( <div > {localCount} </div > ) } function App ( ) { const [key, setKey] = useState (0 ) return ( <div > <button onClick ={() => setKey(key => key + 1)}>Remount</button > <Component key ={key} /> </div > ) }
Children as props / slot pattern Child does not re-render
SlowChild is created by the grandparent and passed as children. Middle App state updates do not re-render SlowChild. The same child written as JSX inside the ticking App would re-render.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 function SlowChild ( ) { console .log ('SlowChild render' ) return <div > slow child</div > } function App ({ children }: { children: React.ReactNode } ) { const [count, setCount] = useState (0 ) useEffect (() => { const intervalId = setInterval (() => { setCount (count => count + 1 ) }, 1000 ) return () => clearInterval (intervalId) }, []) return ( <div > {count} {children} </div > ) } function Grandparent ( ) { return ( <App > <SlowChild /> </App > ) }
With react context and varialbe update NO change
Provider never re-renders with a new value, so context consumers stay at 0.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 const ComponentContext = createContext<number>(0 )const ChildComponent = ( ) => { const count = useContext (ComponentContext ) return ( <div > {count} </div > ) } function App ( ) { let count = 0 useEffect (() => { const intervalId = setInterval (() => { count += 1 }, 1000 ) return () => clearInterval (intervalId) }, []) return ( <div > <ComponentContext.Provider value ={count} > <ChildComponent /> </ComponentContext.Provider > </div > ) }