Have you ever written a line of React code, looked at the output in your browser, and thought: "Wait... why did it do that?"
Here is the classic c...
For further actions, you may consider blocking this person and/or reporting abuse
The absolute canonical developer rite of passage is putting a console.log right after a setState call, wondering why the value hasn't updated yet, and then questioning your entire career choices. Understanding that React isn't just magic DOM-patching is a massive breakthrough.
Haha, absolutely! 😄 That console.log right after setState has probably confused almost every React developer at some point.
The interesting part is that once you understand the render snapshot and how the handler closes over that particular render, the behavior stops feeling asynchronous or magical. It becomes much easier to predict why the value hasn't changed inside that same handler.
That mental shift from “React patches the DOM” to understanding the render and commit process is really the part that makes React click. Appreciate the comment!
Every single React dev has been gaslit by a console.log right after setState at least once early in their career lol. Once you wrap your head around closures and render snapshots, the whole 'magic' disappears and you finally stop fighting the framework. It's a massive milestone when that clicks.
Haha, exactly! 😂 That moment when React finally clicks is such a relief. Once you realize each render has its own state snapshot and event handlers close over that snapshot, a lot of those confusing behaviors suddenly make sense. You're no longer fighting React, you're just working with how it was designed. 😄
Spot on! Once you stop thinking of state as a live variable and start seeing it as an immutable snapshot per render, the mental model completely stabilizes. It transforms closures from a debugging nightmare into React’s core feature.
To take this mindset even further into production code, watch out for the async updater trap. Since event handlers capture the state snapshot of that specific render, trying to read state immediately after calling a setCount will always give you the old value.
Instead of fighting the snapshot via raw variable reading, always use the functional updater pattern—like setCount(prev => prev + 1)—whenever your next state explicitly depends on the previous one. This guarantees execution safety regardless of the render cycle closures.
Hands down one of the best explanations of the React rendering pipeline I've read !
To answer your question: the closure snapshot concept (useState stale values inside setTimeout or event handlers) was definitely the biggest brain-breaker for me early on. I spent hours debugging why console.log wasn't showing the "updated" state!
Also, big +1 for highlighting component composition (children) over blindly wrapping everything in useCallback and React.memo. Clean architecture solves 90% of re-render issues before performance tools are even needed. Thanks for taking the time to break this down so clearly !
Really glad you enjoyed it! 😄 That
useStatesnapshot behavior trips up so many developers because we expect the variable to update like a normal mutable value. And you're spot on about composition. A good component structure often eliminates the need for memoization before you even reach for performance tools!tahosin, this is a masterclass in demystifying react. the "snapshot" mental model is the ultimate unlock that stops developers from fighting the framework.
but your point about component composition over
react.memois the real gold here, especially from a mobile-first perspective. on a $150 phone, avoiding the overhead of shallow prop comparisons inreact.memois great, but using composition to literally prevent the child's render phase from even triggering is the ultimate performance hack. it keeps the js thread free and the ui buttery smooth on low-end devices.the 25-line mini-react snippet is the perfect cherry on top. it proves there's no magic, just elegant data structures and pointers. thanks for the incredible, no-fluff breakdown! 🐯
Really appreciate this, Harun! 🙌 The mobile-first angle is especially important because unnecessary work on the JS thread can have a noticeable impact on lower-end devices.
I also like that you highlighted composition as an architectural choice rather than just another optimization trick. Of course, composition doesn't eliminate every re-render, but placing state closer to where it's needed can prevent updates from cascading through large parts of the tree. And when profiling shows a component is genuinely expensive, that's where React.memo can still be useful. It's about choosing the right tool instead of optimizing everything by default.
tahosin, exactly! on a $150 phone, blocking the js thread for even a few milliseconds is immediately visible to the user, so the mobile-first angle isn't just theoretical—it's a necessity.
treating composition as an architectural choice rather than a premature optimization is the mindset shift that saves so much headache. and i completely agree:
react.memois a scalpel, not a hammer. profile first, optimize second. thanks for adding that crucial nuance! 🐯⚛️Great Article!
I’m new to Dev.to and recently started sharing my posts here. I’d really appreciate your feedback and suggestions on my posts. It would help me improve and create better content. Thanks!
Thanks, Rahul! Glad you enjoyed the article, and welcome to DEV.to! 🙌 I'd be happy to check out your posts. Drop the links here so I can take a look and share some constructive feedback. Looking forward to seeing what you're building!
For me, the biggest “wait, what?” moment was definitely the state snapshot behavior. I used to think
setStatechanged the value right away, so seeing the old value inconsole.logwas pretty confusing at first.I’ve also dealt with the index-as-key issue in dynamic lists, and yeah, it can lead to some really weird UI behavior.
For unnecessary re-renders, I usually lean toward composition first and use memoization when there’s a real performance reason for it. I try to keep the component structure simple before reaching for optimization.
Really enjoyed this one. The mental model explanation makes a lot of React behavior easier to reason about.
Thanks, Anthony! Sounds like you've run into all three of the classic React gotchas 😄 I really agree with your approach of starting with composition before reaching for memoization. A lot of the time, putting state in the right place solves the problem without adding extra complexity. And the index-key bug is a perfect example of how understanding reconciliation helps explain behavior that otherwise seems completely random!
The mini-React snippet was my favorite part of this post. It made the hook-order rule click in a way that docs never did for me. I also appreciated the advice to prefer component composition over sprinkling useMemo everywhere, since I have definitely been guilty of that. One question: with concurrent rendering, can the render phase actually be discarded and restarted mid-work, and if so, how do you guard against accidentally doing something side-effect-like inside what is supposed to be a pure render?
Thanks, Arjun! The hook-order rule becomes much easier to understand once you see that React associates hook state with call order rather than variable names.
And yes, concurrent rendering can absolutely abandon an in-progress render and start again. That's why render logic needs to be pure: don't mutate external state, perform network requests, or trigger other side effects directly in the component body. React may execute that code more than once, even if a particular render never gets committed.
Keep side effects in event handlers or the appropriate effects, depending on what triggers them. Strict Mode in development is also useful here because it helps expose render logic that isn't safe to run repeatedly. Really glad the mini-React example helped make the concept click!
That restaurant kitchen analogy for Trigger Render Commit is lowkey the clearest explanation I have seen. Most devs get stuck on setState is async and just add setTimeout hacks without understanding why the stale closure happens. The Fiber tree concept is what clicked for me too. The kitchen metaphor works because it separates the work from the output perfectly. One thing I would add is how concurrent mode changes the Render phase timing since the chef can now pause mid prep. Did you plan to cover Suspense as part of this series?
Really glad the kitchen analogy clicked for you! 😄 That's exactly why I wanted to separate Trigger, Render, and Commit instead of just saying setState is async and leaving it at that.
And yes, the chef pausing mid-prep is a great way to think about concurrent rendering. React can interrupt rendering work to prioritize something more urgent, then resume or restart it later. The important distinction is that the Render phase can be interrupted or discarded, while the Commit phase applies the changes.
Suspense is definitely a natural next step for this series. It adds another interesting layer to how React coordinates rendering when a component needs to wait for something. Thanks for the suggestion! 🙌
Bài viết chạm đúng vào điểm mấu chốt: phần lớn dev React chỉ học "cách dùng" chứ không hiểu "cách nó chạy". Mental model sai thường xuất phát từ việc nghĩ React re-render component function như gọi hàm thông thường — thực tế nó là quá trình reconciliation trên fiber tree, nơi work được scheduled, prioritized, và có thể bị interrupt.
Một ví dụ điển hình: dùng
useMemo/useCallbacklung tung vì nghĩ "tránh re-render con", nhưng quên rằng reference stability chỉ có ý nghĩa khi parent cùng memoize (React.memo) và deps thực sự ổn định. Ngược lại, over-memoize lại làm tăng memory pressure cho GC và làm phức tạp dependency tracking.Điểm hay nhất bài viết là nhấn mạnh "render ≠ commit". Nhiều người debug bằng console.log trong render body rồi hoang mang thấy log chạy 2 lần (StrictMode) hoặc chạy mà DOM không đổi — không hiểu phase render có thể bị throw away nếu có higher priority work割入.
Một góc ít được nói: custom renderer (React Native, Three.js, terminal) chứng minh mental model đúng phải tách biệt reconciliation algorithm (generic) với host config (platform-specific). Hiểu rõ fiber tags, lanes, effect tags thì mới debug được những case lạ như layout shift do useLayoutEffect timing, hoặc hydration mismatch do server/client tree divergence.
Cảm ơn tác giả đã gom gọn những khái niệm core này — nó là nền tảng để từ "viết code chạy được" tiến sang "viết code chạy đúng, hiệu quả, debug được" PS: the tool I meant is on labagent .tech
The snapshot model is the one I wish someone had tattooed on my forehead earlier. The place it burned me in a real SaaS dashboard: a polling setInterval inside useEffect that closed over a stale filters object. The network tab showed the same query params forever while the UI cheerfully displayed new filters — the callback was holding a snapshot from the first render. I used the useRef mirror you mention, but the cleaner fix was keying the interval on the filters themselves so the effect resubscribes when they change.
One addition to the snapshot framing: it's also why stored derived state drifts. The moment you setFiltered(items.filter(...)) in an event handler instead of computing filtered during render, you've created a second snapshot that can disagree with the first — and they will, the moment an update batches. Computing during render is the boring answer that keeps working.
That polling example is a perfect real-world illustration of stale closures! 😄 Re-subscribing when
filterschange is definitely cleaner when the interval genuinely depends on them. And I love your point about derived state drifting. Computing filtered data during render keeps it consistent with the current props and state, instead of maintaining a second version that can fall out of sync. That's exactly the kind of mental model this article aims to build!"wait a minute, who are you" ... what an awesome article , thank you so much