You know how to create and update state with useState, but what actually happens when state changes? How does React know to update the UI? And why do state updates sometimes behave unexpectedly? In this lesson, you'll learn about the render cycle, how React efficiently updates the DOM, why state updates are batched, and how to avoid common performance issues. Understanding re-renders is key to writing efficient React applications.
What is a "Render"?
In React, a "render" means calling your component function to get the JSX that should be displayed:
function Counter() {
console.log('Rendering Counter...') // This logs on every render
const [count, setCount] = React.useState(0)
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>+</button>
</div>
)
}
// Render sequence:
// 1. Initial render: "Rendering Counter..." (count = 0)
// 2. Click button: setCount(1) is called
// 3. React re-renders: "Rendering Counter..." (count = 1)
// 4. Click again: setCount(2) is called
// 5. React re-renders: "Rendering Counter..." (count = 2)A render is NOT the same as updating the DOM:
- Render - React calls your component function to get JSX
- Commit - React updates the actual DOM based on changes
- React is smart: it only updates DOM elements that actually changed
The Render Cycle
Here's what happens when state changes:
- Trigger - State update triggers a re-render
- Render - React calls your component function
- Commit - React updates the DOM if needed
- Browser Paint - Browser displays the changes
function Example() {
const [count, setCount] = React.useState(0)
console.log('1. Component rendering, count =', count)
const handleClick = () => {
console.log('2. Before setState, count =', count)
setCount(count + 1) // Trigger re-render
console.log('3. After setState, count =', count) // Still old value!
}
console.log('4. Rendering complete')
return <button onClick={handleClick}>{count}</button>
}
// When you click:
// Console output:
// "2. Before setState, count = 0"
// "3. After setState, count = 0" ← State not updated yet!
// "1. Component rendering, count = 1" ← Re-render with new state
// "4. Rendering complete"
// Key insight: State updates are asynchronous
// The component re-renders AFTER the event handler finishesWhy State Updates Are Asynchronous:
React batches multiple state updates together for better performance. Instead of re-rendering after each setState, React waits until your event handler finishes, then does one re-render with all the updates combined. This is much more efficient!
State Update Batching
React automatically batches multiple state updates that happen in the same event handler:
Single Re-render for Multiple Updates
function Example() {
const [count, setCount] = React.useState(0)
const [flag, setFlag] = React.useState(false)
console.log('Rendering...') // How many times does this log?
const handleClick = () => {
setCount(count + 1) // Schedule update
setFlag(!flag) // Schedule update
setCount(count + 2) // Schedule update
// React batches these together
// Only 1 re-render happens, not 3!
}
return (
<div>
<p>Count: {count}</p>
<p>Flag: {String(flag)}</p>
<button onClick={handleClick}>Update</button>
</div>
)
}
// Click button once:
// Console: "Rendering..." (only once!)
// Not three times
// This is automatic batching for better performanceBatching with Function Updates
function Counter() {
const [count, setCount] = React.useState(0)
const incrementThreeTimes = () => {
// ❌ These don't work as expected
setCount(count + 1) // Uses count = 0
setCount(count + 1) // Still uses count = 0
setCount(count + 1) // Still uses count = 0
// Result: count becomes 1, not 3
}
const incrementThreeTimesProperly = () => {
// ✅ These work correctly
setCount(prev => prev + 1) // prev = 0, sets to 1
setCount(prev => prev + 1) // prev = 1, sets to 2
setCount(prev => prev + 1) // prev = 2, sets to 3
// Result: count becomes 3
}
return (
<div>
<p>Count: {count}</p>
<button onClick={incrementThreeTimes}>+3 (Broken)</button>
<button onClick={incrementThreeTimesProperly}>+3 (Works)</button>
</div>
)
}💡 Batching Rule of Thumb
React batches updates in:
✅ Event handlers (onClick, onChange, etc.)
✅ useEffect callbacks
✅ setTimeout/setInterval callbacks (React 18+)
✅ Promise callbacks (React 18+)
In React 18+, batching happens almost everywhere automatically!
When Does React Re-render?
React re-renders a component when:
- State changes - You call a state setter function
- Props change - Parent passes different props
- Parent re-renders - Parent component re-renders
- Context changes - Context value changes (we'll learn this later)
Example: State Changes
function Counter() {
const [count, setCount] = React.useState(0)
console.log('Counter rendering, count =', count)
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>+</button>
</div>
)
}
// Click button:
// Console: "Counter rendering, count = 1"
// Click again:
// Console: "Counter rendering, count = 2"Example: Props Changes
function Child({ value }) {
console.log('Child rendering, value =', value)
return <p>Value: {value}</p>
}
function Parent() {
const [count, setCount] = React.useState(0)
return (
<div>
<button onClick={() => setCount(count + 1)}>
Update Parent
</button>
<Child value={count} />
</div>
)
}
// Click button:
// Console: "Child rendering, value = 1"
// Child re-renders because its prop changedExample: Parent Re-renders
function Child() {
console.log('Child rendering')
return <p>I am a child</p>
}
function Parent() {
const [count, setCount] = React.useState(0)
console.log('Parent rendering')
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>+</button>
<Child /> {/* Child has no props! */}
</div>
)
}
// Click button:
// Console: "Parent rendering"
// Console: "Child rendering" ← Re-renders even though props didn't change!
// When parent re-renders, children re-render by defaultSame Value Updates
What happens if you update state to the same value it already has?
function Example() {
const [count, setCount] = React.useState(0)
console.log('Rendering...')
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(0)}>
Set to 0 (same value)
</button>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
)
}
// Initial render: "Rendering..."
// Click "Set to 0": "Rendering..." (re-renders once)
// Click "Set to 0" again: (no re-render!)
// React is smart:
// First time setting to same value → re-renders (to be safe)
// Second time → skips re-render (optimization)🔍 Object.is Comparison
React uses Object.is() to compare old and new state:Object.is(0, 0) → true (same)Object.is('hi', 'hi') → true (same)Object.is({}, {}) → false (different objects!)Object.is([], []) → false (different arrays!)
This is why you need new objects/arrays to trigger re-renders!
Understanding Render Behavior
Each Render is a Snapshot
function Counter() {
const [count, setCount] = React.useState(0)
const handleClick = () => {
// This render's snapshot: count = 0
setCount(count + 1)
setTimeout(() => {
// This still uses the OLD snapshot!
console.log(count) // Still 0, not 1!
}, 3000)
}
return (
<button onClick={handleClick}>
Count: {count}
</button>
)
}
// Click button, wait 3 seconds:
// Console: 0 (not 1!)
// Why? The timeout captures count from the render when it was created
// Each render has its own "version" of stateState Updates Queue
function Counter() {
const [count, setCount] = React.useState(0)
const handleClick = () => {
// Current render: count = 0
// Queue update: 0 + 1 = 1
setCount(count + 1)
// Queue update: 0 + 1 = 1 (still using old count!)
setCount(count + 1)
// Result: count becomes 1, not 2
}
const handleClickProperly = () => {
// Current render: count = 0
// Queue update: prev(0) + 1 = 1
setCount(prev => prev + 1)
// Queue update: prev(1) + 1 = 2
setCount(prev => prev + 1)
// Result: count becomes 2
}
return (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>+2 (broken)</button>
<button onClick={handleClickProperly}>+2 (works)</button>
</div>
)
}Avoiding Unnecessary Re-renders
Problem: Child Re-renders Too Much
function ExpensiveChild({ data }) {
console.log('ExpensiveChild rendering')
// Imagine this does heavy calculations
return <div>{data}</div>
}
function Parent() {
const [count, setCount] = React.useState(0)
const [name, setName] = React.useState('')
return (
<div>
{/* Unrelated state changes */}
<input
value={name}
onChange={e => setName(e.target.value)}
/>
{/* Child re-renders every time name changes! */}
<ExpensiveChild data={count} />
<button onClick={() => setCount(count + 1)}>
Update Count
</button>
</div>
)
}
// Type in input: "ExpensiveChild rendering" (unnecessary!)
// ExpensiveChild re-renders even though its prop (count) didn't changeSolution: React.memo (Preview)
// We'll learn this in detail later, but here's a preview:
// Wrap component in React.memo
const ExpensiveChild = React.memo(function ExpensiveChild({ data }) {
console.log('ExpensiveChild rendering')
return <div>{data}</div>
})
// Now ExpensiveChild only re-renders when 'data' prop changes!
// Typing in input: (no log - doesn't re-render)
// Click button: "ExpensiveChild rendering" (only when needed)Don't Optimize Too Early!
React is already fast. Don't worry about re-renders until you have an actual performance problem. Premature optimization makes code more complex without real benefits. We'll learn proper optimization techniques in advanced lessons.
Interactive Demo
💡 Tip: Edit the code above and click "Run" to see your changes
Common Re-render Mistakes
1. Creating Objects/Arrays in Render
function Parent() {
const [count, setCount] = React.useState(0)
// ❌ New object on every render
const config = { theme: 'dark' }
return <Child config={config} />
}
// Child re-renders every time because config is a new object!
// ✅ Better: Move outside component
const config = { theme: 'dark' }
function Parent() {
const [count, setCount] = React.useState(0)
return <Child config={config} />
}
// ✅ Or use useMemo (advanced, we'll learn later)
const config = React.useMemo(() => ({ theme: 'dark' }), [])2. Inline Function Props
function Parent() {
// ❌ New function on every render
return <Child onClick={() => console.log('clicked')} />
}
// ✅ Better: Define outside render
function Parent() {
const handleClick = () => console.log('clicked')
return <Child onClick={handleClick} />
}
// Note: This usually doesn't matter for performance
// Only optimize if you have actual performance issues3. Not Using Function Form When Needed
// ❌ Wrong
const increment = () => {
setCount(count + 1)
setCount(count + 1) // Still uses old count
// Result: +1, not +2
}
// ✅ Correct
const increment = () => {
setCount(prev => prev + 1)
setCount(prev => prev + 1)
// Result: +2
}Key Takeaways
- A render means React calls your component function to get JSX
- State updates trigger re-renders asynchronously
- React batches multiple state updates into one re-render
- Each render is a snapshot with fixed state values
- Use function form
setState(prev => ...)when new state depends on previous - Components re-render when state, props, or parent changes
- Setting state to same value may skip re-render (optimization)
- React uses
Object.is()to compare state values - Child components re-render when parent re-renders by default
- Re-renders are usually fast - don't optimize prematurely
- State updates in timeouts/promises capture state from their render
- Understanding re-renders helps you write efficient React code
What's Next?
You now understand how React updates the UI when state changes! You know about the render cycle, batching, and when re-renders happen.
But so far we've been using single state variables. What if your component needs multiple pieces of state? Should you use one useState or several? How do you organize state effectively?
In the next lesson, you'll learn about multiple state variables:
- When to split state into multiple variables
- When to combine state into objects
- How to manage related state
- Best practices for state organization
Get ready to organize your state like a pro! 🎯