Blog #14: The Domino Effect – When one small change crashes the whole Render system
To my early React days, when I wondered why changing one text color made the whole 1000-row table redraw from scratch.
Hey there,
I remember how frustrated you were. You had a complex Dashboard with many child components. You just wanted to add a tiny countdown clock in the corner of the screen. You created a seconds state in the parent component (App), and every second you called setSeconds.
The result? Every second, the entire chart, data table, and user list... everything gave a "slight stutter." Typing in the search bar suddenly felt laggy. You panicked: "Why is React so slow?". You had no idea you'd accidentally triggered an unnecessary "general mobilization" re-render on an application-wide scale.
1. Simple Explanation: The Story of a Family
Imagine a 3-generation family living together. You are the father (Root Component). The supervisor is the grandfather. Your children are the child UIs. Every second, you stand in the middle of the house and shout: "It's now the 32nd second!".
As soon as you shout, everyone in the house—no matter what they're busy doing—must stop, look at you, self-check if the "32nd second" information relates to them, and then continue working. If you shout every second, everyone will go crazy and no one will get anything done properly.
2. Current Perspective: Why 'State Placement' matters?
Before, you thought "if there's data, just push it up to the parent for easy management." But now I understand: The closer the state is to where it's used, the smoother the app.
React is incredibly fast, but comparing the Virtual DOM for thousands of child components every second is a massive waste of resources. When you change state in a Parent Component, by default, all its Child Components must re-render (unless you use memo, but that's a different story about costs we discussed in article #13).
3. Concrete Example: The "Global State" Trap
You used to do this because it was "convenient":
// Junior's way: "Top floor" holds everything
const Dashboard = () => {
const [timer, setTimer] = useState(0); // State changes constantly
const [data, setData] = useState([]); // Heavy data
useEffect(() => {
const id = setInterval(() => setTimer(t => t + 1), 1000);
return () => clearInterval(id);
}, []);
return (
<div>
<TimerDisplay time={timer} />
{/*
Every time timer changes, BigExpensiveChart will re-render
even though data hasn't changed!
*/}
<BigExpensiveChart data={data} />
</div>
);
};
Why is this a problem? Because BigExpensiveChart must redo its chart calculation logic once every second. If the user's machine is weak, they'll see the browser freeze continuously.
Sustainable Approach (State Colocation):
// Push state as low as possible
const Timer = () => {
const [timer, setTimer] = useState(0);
useEffect(() => {
const id = setInterval(() => setTimer(t => t + 1), 1000);
return () => clearInterval(id);
}, []);
return <TimerDisplay time={timer} />;
};
const Dashboard = () => {
const [data, setData] = useState([]);
return (
<div>
<Timer /> {/* Only Timer re-renders every second, Dashboard and Chart stay still! */}
<BigExpensiveChart data={data} />
</div>
);
};
4. Comparison: "Doing it Fast" vs "Doing it Sustainably"
| Criteria | Doing it Fast (Push it all to Parent) | Doing it Sustainably (State Colocation) |
|---|---|---|
| Data Management | Very easy, everything is available at parent | Takes effort to think about where state should be |
| Render Performance | Disaster as the app grows | Absolutely optimized, isolated renders |
| Debugging | Hard to find what causes re-renders | Easy to isolate errors in small components |
| Mindset | Monolithic "monster" style | Atomic style |
Juniors often choose the left column because "it's tedious to pass props." But a Senior will choose the right column because they respect every CPU cycle of the user's computer.
5. Practical Lesson: "Meaningless" Render
I once encountered a bug where when a user typed into an Input in the Header, a list of 500 articles in the Body would lag. It turned out that Input was changing a state at a Context Provider wrapped around the entire application. The whole app was "shaking" every time the user typed a character.
The biggest lesson is: Keep state in the smallest cage possible. If a state only serves one component, never let it escape. "Lift state up" only when you really need to share data between siblings; don't do it out of laziness.
Remember: A smooth application isn't because it uses cool libraries, but because its hundreds of child components know when to "be quiet" and stay still.
Notes for my 25-year-old self, back when I learned to 'restrain' re-renders.
Series • Part 14 of 50