Hard React Output & Behaviour Questions
Question 1: useState — What's the Output?
function Counter() {
const [count, setCount] = useState(0)
function handleClick() { setCount(count + 1) setCount(count + 1) setCount(count + 1) }
return <button onClick={handleClick}>{count}</button> }
Q: After clicking once, what is the count?
Answer: 1
All three setCount(count + 1) calls read the same count = 0 from the closure. All three schedule "set to 0+1 = 1". React batches them into one re-render with count = 1.
Fix: functional updates
setCount(prev => prev + 1) setCount(prev => prev + 1) setCount(prev => prev + 1) // Result: 3 — each update builds on the previous
---
Question 2: useEffect Timing
function App() {
const [val, setVal] = useState(0)
console.log('render', val)
useEffect(() => { console.log('effect', val) })
return <button onClick={() => setVal(v => v + 1)}>Click</button> }
Q: What's the console output on initial load, then after one click?
Answer:
Initial load:
render 0 effect 0
After click:
render 1 effect 1
useEffect runs after render and paint. The console.log in the component body runs during render. No dependency array → runs after every render.
---
Question 3: Stale Closure
function Timer() {
const [count, setCount] = useState(0)
useEffect(() => { const id = setInterval(() => { setCount(count + 1) }, 1000) return () => clearInterval(id) }, [])
return <div>{count}</div> }
Q: What happens after 5 seconds?
Answer: count stays at 1 forever
The effect runs once (empty deps). The interval callback captures count = 0 in its closure. It will always call setCount(0 + 1) — setting count to 1 on every tick.
Fix:
useEffect(() => { const id = setInterval(() => { setCount(prev => prev + 1) // functional update — no closure needed }, 1000) return () => clearInterval(id) }, [])
---
Question 4: Key as Reset
function App() {
const [version, setVersion] = useState(0)
return (
<>
<Form key={version} />
<button onClick={() => setVersion(v => v + 1)}>Reset Form</button>
</>
)
}
function Form() { const [input, setInput] = useState('') return <input value={input} onChange={e => setInput(e.target.value)} /> }
Q: What happens when the Reset button is clicked?
Answer: Form completely resets to empty string
Changing the key makes React treat as a completely different component. It unmounts the old Form (destroying its state) and mounts a brand new one with input = ''.
This is the intentional use of key to reset a component without lifting state up.
---
Question 5: Re-render Triggers
const Parent = () => {
const [count, setCount] = useState(0)
console.log('Parent rendered')
return (
<div>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
<Child />
</div>
)
}
const Child = () => { console.log('Child rendered') return <p>I am a child</p> }
Q: What logs on every button click?
Answer: Both "Parent rendered" and "Child rendered"
React re-renders Parent when count changes. Child is a child of Parent, so React re-renders Child too — even though Child receives no props at all. Children re-render with their parent by default.
Fix: React.memo
const Child = React.memo(() => { console.log('Child rendered') // only logs once — on mount return <p>I am a child</p> })
---
Question 6: useEffect Dependency Bug
function Search({ query }) {
const [results, setResults] = useState([])
useEffect(() => { fetch('/api/search?q=' + query) .then(r => r.json()) .then(data => setResults(data)) }, []) // ← empty deps
return results.map(r => <div key={r.id}>{r.name}</div>) }
Q: What's wrong with this component?
Answer: query is stale — the effect only runs on mount
When the query prop changes (parent types a new search term), the component re-renders but the effect doesn't re-run. The displayed results are from the initial query forever.
Fix: Add query to the dependency array:
useEffect(() => { let cancelled = false fetch('/api/search?q=' + query) .then(r => r.json()) .then(data => { if (!cancelled) setResults(data) }) return () => { cancelled = true } }, [query]) // re-fetch whenever query changes
---
Question 7: Context Re-render
const Ctx = createContext(null)
function Provider() { const [count, setCount] = useState(0) return ( <Ctx.Provider value={{ count, setCount }}> <Consumer /> <button onClick={() => setCount(c => c + 1)}>+</button> </Ctx.Provider> ) }
function Consumer() { const { setCount } = useContext(Ctx) console.log('Consumer rendered') return <button onClick={() => setCount(0)}>Reset</button> }
Q: Does Consumer re-render when count changes?
Answer: Yes — on every count change
{ count, setCount } is a new object reference every render. useContext performs reference equality: different object = all consumers re-render, even if Consumer only reads setCount.
Fix: memoize the value
const value = useMemo(() => ({ count, setCount }), [count]) <Ctx.Provider value={value}>
Now Consumer only re-renders when count actually changes (which it does in this case — but with more fields, only the relevant consumers would re-render).
---
Question 8: The Async State Update
function App() {
const [data, setData] = useState(null)
async function fetchData() { const result = await fetch('/api/data').then(r => r.json()) setData(result) console.log(data) // null or result? }
return <button onClick={fetchData}>Fetch</button> }
Q: What does console.log(data) print after the await?
Answer: null (the old value)
data in the closure is captured at the time fetchData was created — when data = null. setData(result) schedules a state update (re-render), but the closure's data variable is not updated. The new value is only available in the next render's closure.
Practice React behavior questions at [JSPrep Pro](/auth).