| Objects Not Valid as a Child :👈 | 👉:Missing-Key-Props-in-Lists |
Why Is My React Component Not Re-rendering? |
Why Is My React Component Not Re-rendering?
1. Quick Answer
A React component may not re-render when you expect it to if React does not detect a relevant state or prop change, the component is not subscribed to the data that changed, or the update logic is incorrect.
Common reasons include:
- Updating a state variable directly instead of using its setter function.
- Modifying an object or array without creating a new reference.
- Updating a regular JavaScript variable instead of React state.
- Mutating a prop instead of passing a new value from the parent.
- Using
React.memoor a custom comparison function that prevents an expected re-render. - Expecting a component to update when a context, external store, or other data source changes without subscribing to it correctly.
- Incorrectly implementing an effect or event handler.
Important: Calling a state setter requests an update. React may skip rendering when the new state is identical to the current state according to its comparison rules, and it may batch multiple updates together.
2. Table of Contents
- Concept Explanation
- Diagram
- Real-World Example
- Code Examples and Solutions
- Common Mistakes
- When to Use It
- When NOT to Use It
- Interview Questions
3. Concept Explanation
What Is Re-rendering in React?
Re-rendering occurs when React calls a component function again to calculate what the user interface should look like.
React commonly re-renders a component when:
- Its state changes.
- Its parent re-renders and React evaluates the child component.
- A context value it consumes changes.
- An external store subscription notifies React of a relevant change.
However, a component wrapped in React.memo may skip a parent-triggered re-render when its props have not changed according to the memoization comparison. State and context updates used by the component can still cause it to re-render.
1. Updating State Directly
React state should be updated using the setter returned by useState.
Incorrect:
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
function increment() {
count = count + 1;
}
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</div>
);
}
This code is incorrect because the state variable is not an ordinary variable that you should assign to directly. Depending on the exact code, attempting to assign to a state binding may also cause a JavaScript error.
Correct:
function increment() {
setCount(count + 1);
}
The setter requests a state update, allowing React to render the updated count.
2. Mutating Objects or Arrays
React state may contain objects and arrays. You should treat these values as immutable and create a new object or array when updating them.
Incorrect:
user.name = "Priya";
setUser(user);
This changes the existing object and passes the same reference back to React. React may skip the update because the new state is identical to the current state according to Object.is.
Correct:
setUser(previousUser => ({
...previousUser,
name: "Priya"
}));
This creates a new object reference, allowing React to detect the state change.
3. Updating a Regular Variable
A regular JavaScript variable does not trigger a React re-render when its value changes.
function Counter() {
let count = 0;
function increment() {
count++;
console.log(count);
}
return (
<button onClick={increment}>
Count: {count}
</button>
);
}
The variable changes inside the event handler, but React does not know that it should update the interface. A later render would also create a new local variable initialized to zero.
Solution: Use useState for values that should affect the rendered interface.
4. React.memo Preventing an Expected Re-render
React.memo can skip a component re-render when its props have not changed according to its comparison function.
import { memo } from "react";
const User = memo(function User({ name }) {
console.log("User rendered");
return <p>{name}</p>;
});
If the parent re-renders but passes the same primitive value for name, React can skip rendering the memoized child.
This is normal behavior, not necessarily a bug. If the child depends on changing data that is not represented in its props, state, or context, review how that data reaches the component.
4. Diagram
How React Decides Whether to Re-render
User Action or Data Change
|
v
Was React Notified of the Change?
|
+---+---+
| |
Yes No
| |
v v
State, Props, No React Update
Context, or Requested
Subscription
Changes
|
v
Does React Need to Render?
|
+---+---+
| |
Yes No
| |
v v
Render May Skip
Component Rendering
|
v
Update the UI if Needed
Key Principle
React must be able to detect a relevant change through its state, props, context, or subscription mechanisms. Changing data outside those mechanisms does not automatically notify React.
5. Real-World Example
Imagine a shopping application that displays a product's quantity. The user clicks an Increment button, but the displayed quantity does not change.
Incorrect implementation:
function ProductQuantity() {
const [quantity, setQuantity] = useState(1);
function increaseQuantity() {
quantity++;
}
return (
<div>
<p>Quantity: {quantity}</p>
<button onClick={increaseQuantity}>
Increment
</button>
</div>
);
}
The code attempts to modify the state variable directly rather than using its setter.
Correct implementation:
import { useState } from "react";
function ProductQuantity() {
const [quantity, setQuantity] = useState(1);
function increaseQuantity() {
setQuantity(previousQuantity => previousQuantity + 1);
}
return (
<div>
<p>Quantity: {quantity}</p>
<button onClick={increaseQuantity}>
Increment
</button>
</div>
);
}
export default ProductQuantity;
Now the setter requests an update, and React renders the new quantity.
6. Code Examples and Solutions
Solution 1: Use the State Setter Function
Always use the setter returned by useState to update state.
Incorrect:
const [count, setCount] = useState(0);
count = count + 1;
Correct:
const [count, setCount] = useState(0);
setCount(count + 1);
For updates based on the previous state, prefer the functional updater:
setCount(previousCount => previousCount + 1);
This is particularly useful when several updates may be queued before React renders again.
Solution 2: Create a New Object When Updating State
Do not mutate an existing object and pass the same reference back to React.
Incorrect:
const [user, setUser] = useState({
name: "Ravi",
age: 25
});
function updateName() {
user.name = "Priya";
setUser(user);
}
Correct:
const [user, setUser] = useState({
name: "Ravi",
age: 25
});
function updateName() {
setUser(previousUser => ({
...previousUser,
name: "Priya"
}));
}
The spread operator copies the existing properties into a new object, and the new name value replaces the previous one.
Important: Object spread performs a shallow copy. If you update a nested object, create new references for the relevant nested objects as well.
Solution 3: Create a New Array When Updating State
Array methods such as push(), pop(), and splice() mutate the existing array. Avoid using them to mutate React state directly.
Incorrect:
const [items, setItems] = useState(["Apple", "Banana"]);
function addItem() {
items.push("Orange");
setItems(items);
}
Correct:
const [items, setItems] = useState(["Apple", "Banana"]);
function addItem() {
setItems(previousItems => [
...previousItems,
"Orange"
]);
}
This creates a new array containing the existing items and the new item.
Solution 4: Use State Instead of a Regular Variable
If a value affects the user interface, store it in React state rather than an ordinary local variable.
Incorrect:
function Counter() {
let count = 0;
function increment() {
count++;
}
return (
<button onClick={increment}>
Count: {count}
</button>
);
}
Correct:
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
function increment() {
setCount(previousCount => previousCount + 1);
}
return (
<button onClick={increment}>
Count: {count}
</button>
);
}
React tracks the state and can re-render the component when its value changes.
Solution 5: Check React.memo and Custom Comparison Functions
If a component is wrapped in React.memo, React normally compares its previous and next props using a shallow comparison based on Object.is.
import { memo, useState } from "react";
const User = memo(function User({ name }) {
console.log("User rendered");
return <p>Name: {name}</p>;
});
function App() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
<User name="Ravi" />
</div>
);
}
export default App;
When the parent updates count, the memoized User component can skip rendering because its name prop has not changed.
This is expected. If you believe the component should update, verify that the data it depends on is actually changing and being passed through its props, state, or context.
Also inspect custom comparison functions passed to React.memo. An incorrect comparison function can tell React that props are equivalent even when a relevant value has changed.
Solution 6: Understand State Batching and Previous Values
React may batch multiple state updates together. A state variable in the current render does not immediately change after calling its setter.
function Counter() {
const [count, setCount] = useState(0);
function incrementTwice() {
setCount(count + 1);
setCount(count + 1);
}
return (
<div>
<p>Count: {count}</p>
<button onClick={incrementTwice}>
Increment Twice
</button>
</div>
);
}
In this example, both calls use the same count value from the current render. If the count starts at zero, both request the value one, so the final count becomes one rather than two.
Correct approach:
function incrementTwice() {
setCount(previousCount => previousCount + 1);
setCount(previousCount => previousCount + 1);
}
Each updater receives the result of the previous queued update, so the count increases by two.
Solution 7: Use useEffect Only When an Effect Is Needed
useEffect is useful for synchronizing a component with external systems, such as browser APIs, subscriptions, or network connections. It is not normally necessary just to make React state updates trigger a re-render.
Unnecessary approach:
const [count, setCount] = useState(0);
const [message, setMessage] = useState("");
useEffect(() => {
setMessage(`Count: ${count}`);
}, [count]);
If the message is always derived from the count, you can calculate it during rendering instead:
const [count, setCount] = useState(0);
const message = `Count: ${count}`;
This avoids maintaining redundant state.
Solution 8: Check Whether a Component Is Receiving Updated Props
A child component re-renders when React evaluates it with changed props, unless an applicable optimization allows React to skip rendering.
Example:
function UserProfile({ user }) {
return (
<div>
<h2>{user.name}</h2>
</div>
);
}
function App() {
const [name, setName] = useState("Ravi");
const user = {
name: name
};
return (
<div>
<button onClick={() => setName("Priya")}>
Change Name
</button>
<UserProfile user={user} />
</div>
);
}
When name changes, the parent re-renders, creates a new user object, and passes it to the child. The child displays the updated name.
In a larger application, ensure the parent really passes the updated data and that no mutation or memoization logic is preventing the expected update.
Solution 9: Use useRef Only for Values That Do Not Need to Trigger Rendering
A ref stores a mutable value without triggering a re-render when that value changes.
const countRef = useRef(0);
function increment() {
countRef.current++;
}
This is appropriate for values such as timer IDs or mutable information that does not directly determine the rendered UI.
If the displayed count must update, use state instead:
const [count, setCount] = useState(0);
function increment() {
setCount(previousCount => previousCount + 1);
}
Solution 10: Debug the Component Systematically
If a component still does not re-render as expected, follow these steps:
- Identify the state, prop, or context value that should change.
- Confirm that the event handler or update function is actually called.
- Check that state is updated using its setter function.
- Check that objects and arrays are updated immutably.
- Inspect whether the component is wrapped in
React.memoor another optimization. - Verify that the parent passes updated props.
- Check context and external-store subscriptions if the data comes from those sources.
- Use React Developer Tools to inspect component props, state, and render behavior.
7. Common Mistakes
Mistake 1: Mutating State Directly
Incorrect:
user.age = 26;
setUser(user);
Correct:
setUser(previousUser => ({
...previousUser,
age: 26
}));
Mistake 2: Expecting State to Change Immediately
Incorrect expectation:
setCount(count + 1);
console.log(count);
The logged value belongs to the current render and does not immediately change when the setter is called.
Better: Use a functional updater when the next state depends on the previous state. If you need to observe the committed value after a render, use an effect with the appropriate dependency.
useEffect(() => {
console.log("Updated count:", count);
}, [count]);
Mistake 3: Using a Regular Variable for UI State
A regular variable does not notify React when it changes. Use useState for values that must affect rendering.
Mistake 4: Assuming Every Parent Update Must Re-render Every Child
React can skip rendering a memoized child when its props are unchanged. This is often a performance optimization rather than a bug.
Mistake 5: Mutating Nested Objects
Creating a new top-level object is not enough if you directly mutate a nested object and reuse its reference.
Incorrect:
user.address.city = "Hyderabad";
setUser({
...user
});
Correct:
setUser(previousUser => ({
...previousUser,
address: {
...previousUser.address,
city: "Hyderabad"
}
}));
This creates new references for both the top-level user object and its address object.
Mistake 6: Using useEffect to Force Rendering
Adding an effect that updates state unnecessarily can create redundant renders or an infinite update loop.
First determine why React is not receiving or detecting the expected change. Fix the state, props, or subscription logic rather than forcing additional updates.
8. When to Use It
| Situation | Recommended Approach |
|---|---|
| A displayed value changes | Use useState and its setter. |
| An object in state changes | Create a new object instead of mutating the existing one. |
| An array in state changes | Create a new array using spread, map(), filter(), or another immutable operation. |
| A value depends on previous state | Use a functional state updater. |
| A child receives new data | Verify the props passed by the parent. |
| A memoized component skips rendering | Inspect props and comparison logic. |
| A context value changes | Verify the provider value and context consumption. |
| A mutable value does not affect the UI | Use state if the value should trigger rendering; use a ref if it should not. |
9. When NOT to Use It
1. Do Not Force Re-renders Without Understanding the Cause
A forced re-render may hide the real issue, such as state mutation or incorrect prop handling. Identify and correct the underlying problem first.
2. Do Not Use State for Every Variable
Values that do not affect the rendered UI do not necessarily need to be state. For example, timer IDs can often be stored in refs.
3. Do Not Use useEffect for Every Derived Value
If a value can be calculated from existing state or props during rendering, calculate it directly instead of storing a duplicate value and synchronizing it with an effect.
4. Do Not Remove React.memo Without Evidence
React.memo is not inherently a problem. First inspect whether the memoization comparison is correct and whether the component's props represent all relevant inputs.
5. Do Not Mutate State Even If a Re-render Seems to Work
Mutating state can cause unpredictable behavior because React relies on state identity and render snapshots. Prefer immutable updates consistently.
10. Interview Questions
Q1. Why is my React component not re-rendering?
Possible causes include directly mutating state, updating a regular variable instead of state, passing unchanged props to a memoized component, or failing to subscribe to the data source that changed.
Q2. Does calling a state setter always cause a re-render?
Calling a state setter requests an update, but React may skip rendering when the next state is identical to the current state according to Object.is. React may also batch multiple updates.
Q3. Why should state objects be treated as immutable?
Immutable updates create new object references, making changes easier for React to detect and reducing unexpected behavior caused by shared mutable data.
Q4. What is the difference between useState and useRef?
useState stores a value that participates in rendering and requests an update when its setter changes the state. useRef stores a mutable value that persists between renders without triggering a re-render when its current property changes.
Q5. Why does React.memo sometimes prevent a component from rendering?
React.memo can skip a parent-triggered render when the component's props have not changed according to its comparison. The component can still re-render when its own state or consumed context changes.
Q6. What is state batching?
State batching allows React to group multiple state updates into fewer renders. This improves performance, but it means state variables in the current render do not immediately reflect newly requested values.
Q7. Why use a functional state updater?
A functional updater receives the previous state and calculates the next state from it. This is useful when updates depend on previous values or multiple updates are queued.
setCount(previousCount => previousCount + 1);
Q8. Can changing a prop directly trigger a re-render?
Props are inputs supplied by a parent. A child should not mutate its props. When the parent provides changed props, React can re-render the child unless an applicable optimization skips the render.
Q9. How do you debug unexpected rendering behavior?
Use React Developer Tools, inspect state and props, verify update handlers, check for mutations, and review memoization and subscriptions. Logging inside the component can help, but remember that development Strict Mode may invoke render logic more than once to detect problems.
Q10. What is the difference between re-rendering and updating the DOM?
A re-render means React calls the component to calculate its output again. React then compares the new output with the previous output and updates the DOM only where necessary. A re-render does not necessarily mean that the browser DOM changes.
Summary
A React component may not re-render as expected when React does not receive or detect the relevant change, or when an optimization correctly skips unnecessary work.
Remember these key practices:
- Use state setters instead of assigning to state variables directly.
- Create new objects and arrays when updating state.
- Use functional updaters when the next state depends on the previous state.
- Understand state batching and render snapshots.
- Inspect
React.memoand custom comparison functions. - Verify that props, context, and external-store subscriptions provide updated data.
- Use React Developer Tools to diagnose unexpected behavior.
Best practice: Make state updates immutable, ensure the component receives the data it depends on, and let React manage rendering rather than forcing re-renders unnecessarily.
| Objects Not Valid as a Child :👈 | 👉:Missing-Key-Props-in-Lists |