What React's Component Model Actually Does Under the Hood

The counter that resets when it shouldn't is not a bug — it's the whole model
Build a toggle that swaps between two sibling components rendered at the same spot in a tree, give one of them local state, and watch what happens when you switch away and back: the state is gone. Not because React garbage-collects idle components, and not because you did anything wrong with useState. It's gone because React ties state to a component's position in what it calls the render tree, not to the component's identity as you understand it. Swap the type rendered at that position, or change its key, and React tears down the old instance and its state along with it — deliberately [1]. Understanding that one rule resolves most of the "why did my input clear itself" and "why do two identical widgets not share data" questions that show up in every React codebase, including ones written by people who've shipped it for years.
This is the part most starter guides skip, because it requires reading react.dev/learn/preserving-and-resetting-state rather than reciting "React uses a virtual DOM" from memory. That phrase gets said constantly and explains almost nothing about how the library actually decides what to redraw, when a component keeps its memory between renders, or why frontend work as a discipline is mostly about managing tree identity and not about writing HTML with extra steps.
State lives in React, not in the component, and position is the key
Here's the mental model the docs actually teach, and it's more precise than "state belongs to the component":
Tick the checkbox after clicking the button a few times, and the score survives. React sees the same component type (Counter) rendered at the same position in the tree on both branches of the ternary, so it treats it as the same instance and keeps the state attached to it — the props might change, but the identity doesn't [1]. Now change one branch to render a different element type, say a <p>, and the state is destroyed the moment you flip the condition, because React removed a Counter from that position and put something else there. Nothing about the value 0 versus a nonzero score matters; only the shape of the tree at that address does.
This is also why lists need a stable key that isn't the array index when the list can reorder. Without a real key, React matches old and new children by position, and reordering the array looks — from React's diffing algorithm's perspective — exactly like editing every item's props in place, which routes each item's remembered state to the wrong row [1]. Give it a key derived from something durable (a database id, not the loop counter), and the same algorithm correctly recognizes "this is the item that moved" instead of "these five props changed."
The word "virtual DOM" is doing less work in 2026 than it used to
The mechanic that used to be described as "React builds a virtual DOM tree, diffs it against the previous one, and patches the real DOM" is still true as a description of the reconciliation algorithm, but it's no longer the interesting part of what React does, and it hasn't been the thing you optimize around by hand since React Compiler went stable. Meta shipped React Compiler 1.0 on October 7, 2025, as a build-time tool that statically analyzes your component code and inserts memoization automatically, on both React and React Native, without requiring you to rewrite anything [2]. The team describes it as having been battle-tested internally, including on the Meta Quest Store, before the stable tag went out [2][3].
What that changes in practice: the manual useMemo / useCallback / React.memo triage that used to be half of "advanced React" — deciding which components re-render too often and wrapping the expensive ones — is now something the compiler does for you if it's wired into your build. Vite, Next.js, and Expo all shipped templates with compiler support as launch partners [2]. If you're starting fresh in 2026 you can reasonably skip learning the manual memoization dance as a first-week topic and come back to it once you understand why it existed.
The diffing model itself hasn't disappeared — the compiler doesn't replace reconciliation, it reduces the amount of redundant re-rendering that reconciliation has to chew through. So the phrase "virtual DOM" is still technically accurate as a description of the tree React keeps in memory to compute the minimal set of DOM mutations [4], but leading with it as the headline fact about React, the way most intro material still does, buries the thing that will actually determine how your code looks: whether the compiler's static analysis can see through your component well enough to memoize it, which mostly means writing components whose props and returned JSX follow normal, non-mutating JavaScript patterns rather than clever ones.
Props flow down, and the argument about where state should live is the whole job
Props and state get taught as a pair, and the distinction genuinely is simple: props are values a parent hands down and the child cannot change; state is a value a component owns and can change itself, triggering a re-render when it does. The part that isn't simple, and that no amount of reading the definitions prepares you for, is deciding which component should own a given piece of state when two siblings both need it.
SearchBox and ResultsList are siblings. Neither can see the other's data directly — React has no built-in sibling-to-sibling channel — so the value has to live in their common parent and get handed down as props, with a callback handed down alongside it so the child can ask the parent to change it. This is "lifting state up," and it's not an advanced pattern, it's the only pattern React gives you for two components to share data without reaching for something external. The entire early debate over Redux, then Context, then Zustand and Jotai, is downstream of this one constraint: prop-drilling a value through four layers of components that don't use it, just to get it from where it's owned to where it's needed, gets painful fast, and each of those libraries is a different answer to "how do we skip the drilling." None of them changes the underlying rule that state has one owner and everyone else gets a copy of the value plus a way to ask for a change.
React's built-in escape hatch for this, without a third-party library, is useContext, and it's worth knowing its actual cost: every component that reads a context re-renders when that context's value changes, with no built-in way to subscribe to only part of it. For values that change often — anything typed into a form, scroll position, live data — that makes Context a poor fit and a state library or colocated state a better one. For values that barely change — the current theme, the logged-in user, a feature flag set once at boot — Context is exactly the right tool and reaching for a library is overkill.
Setting up a project in 2026 is not what most tutorials still show
If you're following material written before February 2025, it probably tells you to run npx create-react-app. Don't. The React team formally deprecated Create React App on February 14, 2025: new installs now print a deprecation warning, and the announcement states plainly that CRA has no active maintainers and that the frameworks solving the problems it never did — routing, data fetching, server rendering — already exist [5]. CRA still runs in maintenance mode if you inherit an old project, but it is not the starting point for a new one.
The current guidance splits into two paths, and the choice matters more than most starter content admits:
A framework, if you want routing, data fetching, and server rendering handled for you: Next.js's App Router, React Router v7, or Expo for native. React's own team recommends starting here for anything that will eventually need more than a single client-rendered page, because server rendering reduces the JavaScript shipped to the browser and improves First Contentful Paint and Interaction to Next Paint versus a pure client-side app [5].
A build tool with no framework opinions, if you want a single-page app and nothing else: Vite is the option React's docs point to as the direct, lighter-weight successor to CRA's role [6]. As of the current create-vite package, Vite requires Node.js 20.19+ or 22.12+ — check this against whatever Node LTS you're actually running, because it has moved twice in two years and older tutorials will quote 16+ or 18+ [7].
That's the whole setup for a client-rendered app with hot module reloading, TypeScript, and no Webpack config to write. If you later need server rendering, you migrate to a framework rather than bolting SSR onto Vite by hand — that's not a limitation to fight, it's a signal that your app has grown past what a bare build tool is for.
Composition, not configuration, is what "reusable component" actually means
The instinct when a component needs to vary is to add a prop for every variation: <Button variant="primary" size="large" icon="check" iconPosition="left" loading={false}>. This works until it doesn't, and the point where it doesn't is precisely when two of those props need to interact in a way you didn't anticipate — a loading state that should hide the icon, say — and now the component's internal logic is a small decision tree instead of a rendering function.
The alternative React actually encourages, via the children prop, is composition:
Instead of teaching Button every possible combination of icon and loading state, you hand it arbitrary children and let the caller assemble the combination it needs. This is the same instinct behind slots in other UI systems, and it's why well-designed React component libraries tend to expose a handful of small, composable pieces (Dialog, Dialog.Header, Dialog.Body) rather than one component with forty boolean props. The trade-off is real: composition pushes more decisions to the call site, which is more verbose for the simple case and much better for the case you didn't foresee. Neither approach is universally correct; a component that will only ever have three fixed variants is better served by a variant prop than by a composition API nobody needs.
The mistake that costs the most time is mutating state directly
This will often appear to work in development, then silently fail to re-render in production, or re-render inconsistently depending on what else touched the array that render cycle. React decides whether to re-render by comparing the new state value to the old one with Object.is, which for arrays and objects checks reference equality, not content [1]. todos.push mutates the same array reference, so the "new" value and the old value are the same object as far as React can tell, and it may correctly conclude nothing changed. The fix is always to construct a new reference:
This is not a style preference. It's the direct, mechanical consequence of how change detection works, and it's the single most common cause of "my state updated but the screen didn't" bug reports in every React codebase that hasn't yet adopted the compiler's stricter linting, and even the compiler's lint rules won't save you from mutating a plain object that isn't tracked by a hook.
Frontend work in 2026 also means dealing with what breaks in production
It's worth being honest that this is not a purely academic exercise. On December 3, 2025, the React team disclosed an unauthenticated remote code execution vulnerability in React Server Components, patched in versions 19.0.1, 19.1.2, and 19.2.1, with a follow-up disclosure eight days later covering a denial-of-service and source-code-exposure issue found while researchers were probing the first patch [8][9]. If you're running an app on Server Components in that window, "keep dependencies current" wasn't advice, it was the difference between being exposed and not. That's the part of frontend engineering that never shows up in a components-and-props tutorial: you're not just building UI, you're operating a piece of infrastructure that renders on a server, executes third-party code, and needs the same patching discipline as any backend service. The framework choice you made in the section above — whether you're using Server Components at all — determines whether an advisory like that is your problem or irrelevant to you.
Separately, on October 7, 2025, the React team announced it was forming the React Foundation, and that foundation officially launched under the Linux Foundation on February 24, 2026, with governance now shared across Meta, Amazon, Expo, Callstack, Microsoft, Software Mansion, and Vercel rather than sitting with Meta alone [10][11]. Practically, this doesn't change any API you write, but it does mean release cadence and priorities are now a multi-company negotiation rather than one company's roadmap — worth knowing if you're betting a career on the platform's stability.
Whether this discipline fits you is a question about the failure mode you'd rather have
The honest version of "should I do frontend engineering" isn't about talent, it's about which category of problem you find tolerable to debug for hours. Backend and infrastructure failures tend to be deterministic once you find them: a query is slow, a lock is held too long, a config value is wrong, and the fix is usually clean once located. Frontend failures are disproportionately about state you can't fully see — a re-render that happened for a reason buried three components up the tree, a CSS rule from a stylesheet you didn't write cascading into a component you did, a browser that implements a spec slightly differently than the one you tested in, a race between two async state updates that only reproduces on a slow connection. None of that is harder than backend work; it's a different texture of hard, and some people find it engaging and some find it maddening.
Concretely, you'll spend real time on: reading component trees to figure out why something re-rendered when it shouldn't have, per the position-and-identity rule above; deciding where state should live every time two components need to share data; reasoning about async operations that can resolve out of order (a slow request for page 1 completing after a fast request for page 2 already updated the UI); and working within design constraints set by people who don't know or care what useEffect's dependency array does. If the idea of debugging "why did this input lose focus when the parent re-rendered" for forty minutes sounds like an interesting puzzle rather than a waste of an afternoon, that's a better signal than any list of required skills. If it sounds exhausting, the underlying skills — decomposition, working with asynchronous flows, reasoning about state machines — transfer cleanly to backend and systems work, where the same rigor applies to problems that hold still a little better while you look at them.
What doesn't transfer is assuming component syntax is the hard part. It isn't, and treating it as though it were is how people end up three months into a frontend role still surprised by behavior that the render-tree model would have predicted on day one.
