Skip to main content

Command Palette

Search for a command to run...

🔍 How React Works Under the Hood

Updated
8 min read
🔍 How React Works Under the Hood
U

We're building tools that help teams migrate from React class components to functional components — starting with free education, moving toward powerful automation. Creator of Unclass, a CLI and analyzer to bring legacy code into the modern React era. I write weekly about React migration, code transformation, and automation with TypeScript.

🛠️ Purpose: This guide is a foundational deep-dive into React’s internals, crafted for software artisans preparing to migrate class components to modern function components. We’ll walk through React’s core concepts step by step – from the Virtual DOM and JSX to how React renders, reconciles, and commits updates – and highlight the key differences between class and function components. Each section is organized with clear headers, lists, and emoji markers to make the journey easy to follow and reference.

🖼️ Virtual DOM: Declarative Rendering with In-Memory Diffing

React maintains an idealized representation of the UI in memory, often called the Virtual DOM (VDOM). In this model, your React component tree is represented by lightweight JavaScript objects (React elements) rather than direct DOM nodes. When your state or props change, React computes a new virtual DOM and reconciles it with the previous one to determine what real DOM updates are needed. This diffing process lets React batch and minimize expensive DOM operations. In the React docs:

“The virtual DOM (VDOM) is a programming concept where an ideal, or ‘virtual’, representation of a UI is kept in memory and synced with the ‘real’ DOM… This process is called reconciliation.”

By abstracting the DOM updates, React lets you write code declaratively (“tell React what the UI should be”) while it handles the imperative work (“React makes sure the DOM matches that state”). Because JavaScript object updates are much faster than raw DOM manipulation, React can compute changes offscreen and apply only the necessary updates to the real DOM. In practice, a JSX expression compiles into a React element – a plain object with keys like type, props, and children. On each update, React compares the new element objects with the old ones (using component type and keys) and keeps, moves, or removes DOM nodes as needed.

📝 JSX and React Elements: From Syntax to Objects

React’s JSX syntax is just syntactic sugar that ultimately becomes calls to React.createElement(...). For example, JSX like <h1>Hello</h1> is compiled (by Babel) into React.createElement("h1", null, "Hello"). Each call produces a React element – a JS object describing that element. React elements are immutable descriptors of the UI. A simplified element object looks like:

{ 
  $$typeof: Symbol(react.element), 
  type: 'h1', 
  props: { children: 'Hello', className: 'title' }, 
  key: null, 
  ref: null 
}

React elements may also represent component types (functions or classes) and can contain nested children, keyed lists, etc.

When you update state, React “re-renders” by calling your components (or functions) and producing a new tree of these element objects. React then diffs the old vs new trees:

  • It assumes elements of different types produce completely different subtrees (so it will tear down and rebuild those nodes).

  • It uses key props to match corresponding items in lists.

As one write-up describes, “Whenever something changes, React tries to compare the previous object with the current and decides what to keep, discard and re-create.” In other words, JSX → createElement gives objects, and React’s reconciliation steps determine the minimal set of DOM updates needed to align the UI with those objects.

⚙️ React Fiber, Rendering & Reconciliation

React’s Fiber architecture is its internal reconciliation engine (since React 16). Think of the Fiber tree as a linked data structure of “work units” corresponding to your component tree. Fiber’s headline feature is incremental and prioritized rendering. In the render phase, React traverses your tree of components (via your render() methods or function bodies) and creates/updates the Fiber nodes and the virtual DOM. This phase builds a “work-in-progress” tree and figures out what changed, without touching the real DOM yet.

Key points about Fiber:

  • Render Phase (Reconciliation): React creates or updates fiber nodes for each element, computes the diff, and prepares a list of updates. This phase is interruptible: React can pause, yield control (to keep the UI responsive), and resume work. It uses a priority scheduler to update urgent changes (like user input) first. Importantly, no DOM mutations happen yet.

  • Commit Phase: Once the diff is ready, React enters the commit phase to apply all changes to the real DOM synchronously. In this phase React applies the updates (inserts, updates, or removes actual DOM nodes) and invokes lifecycle methods or effect callbacks.

These phases ensure React does most heavy work (diffing) off-DOM and only syncs with the browser when ready. By design, reconciliation (diffing) and rendering are separate concerns: the former figures out what should change (per-component, in virtual DOM), and the latter (ReactDOM or React Native renderer) actually updates the target environment. This separation even allows React to target different platforms (browser DOM vs native) with the same core algorithm.

📋 Rendering Steps (Trigger → Render → Commit)

In practice, any React update goes through three steps:

  1. Trigger: React begins an update because the root component was mounted, or state/props changed. (e.g. calling root.render(<App/>) or setState/useState).

  2. Render (Reconciliation): React calls your components’ render functions (or executes function components) to produce a new element tree. It recursively descends into children, building a fresh virtual DOM and Fiber tree. React then diffs this new tree against the previous one to determine changes.

  3. Commit: React applies the changes: it updates the real DOM via minimal operations and runs any queued effects or lifecycles.

⏱️ Component Lifecycle (Mount, Update, Unmount)

Each component (whether class or function with Hooks) goes through mount → update(s) → unmount phases, during which side-effects may run.

Class Components

  • constructor()

  • render()

  • componentDidMount() (after initial commit)

  • shouldComponentUpdate()

  • componentDidUpdate()

  • componentWillUnmount()

Function Components (via Hooks)

  • useEffect(() => { ... }, []) → mount effect

  • useEffect(() => { ... }, [dep]) → update effect

  • return () => { ... } inside useEffect → cleanup on unmount

Function components don’t have an instance or lifecyle methods, but React still manages mount/update/unmount. The Hooks system provides a hook into these stages. useEffect behaves like componentDidMount + componentDidUpdate, and its return value behaves like componentWillUnmount.

⚖️ Class vs Function Components

Key differences in how they work internally and how you write them:

  • Syntax and State:
    Class: this.state = { ... } + this.setState()
    Function: const [state, setState] = useState(...)

  • Lifecycle and Effects:
    Class: lifecycle methods (componentDidMount, etc.)
    Function: useEffect, useLayoutEffect, useRef, etc.

  • Boilerplate and Readability:
    Function components are generally more concise and less error-prone. They avoid this binding issues, and logic can be split across multiple hooks.

  • Concurrent Features Compatibility:
    New React features (e.g. Suspense, transitions) are designed with function components in mind.

🧰 Hook Classification for Migration Clarity

As you prepare to transition from class components to functional ones, understanding Hooks is non-negotiable. They’re the modern gateway to managing state, effects, and performance in React.

Let’s look at Hooks through two lenses: what they do and how complex/important they are during development and interviews. This classification helps map them to class lifecycle equivalents and identify when each is appropriate.

🧭 Classification by Meaning

🗂️ State Management Hooks

  • useState – Add local component state.

  • useContext – Share values across the tree without prop drilling.

  • useReducer – Handle complex state logic with reducers (Redux-style).

🔁 Side Effect Hooks

  • useEffect – Run side effects after render (e.g., fetches, subscriptions).

  • useLayoutEffect – Run effects before the browser paints.

  • useInsertionEffect – Inject styles or other low-level layout changes before all layout effects.

  • useImperativeHandle – Customize exposed refs with forwardRef.

⚡ Performance Optimization Hooks

  • useMemo – Memoize expensive calculations.

  • useCallback – Memoize function instances between renders.

  • useDeferredValue – Defer updates for low-priority state.

  • useTransition – Mark parts of updates as non-urgent.

📦 Reference and Mutable Access Hooks

  • useRef – Persist a mutable value across renders (e.g., DOM refs).

  • useId – Generate unique IDs across renders/server.

  • useSyncExternalStore – Subscribe to external sources (e.g., global state or stores).


🧮 Classification by Complexity, Lifecycle, and Importance

HookComplexityLifecycle PhaseDeveloper Importance
useStateBasicUpdate (triggers re-render)⭐️ High
useEffectBasicMount, Update, Unmount⭐️ High
useContextBasicAlways active (re-renders on change)⭐️ High
useReducerIntermediateSimilar to state update⭐️ Medium
useCallbackIntermediateMemoization⭐️ Medium
useMemoIntermediateMemoization⭐️ Medium
useRefIntermediatePersistent between renders⭐️ Medium
useLayoutEffectAdvancedBefore paint⭐️ Low
useImperativeHandleAdvancedWith forwarded refs⭐️ Low
useDebugValueAdvancedDebugging custom hooks⭐️ Low
useTransitionAdvancedMarks updates as "transition"⭐️ Low–Medium
useDeferredValueAdvancedDelayed updates⭐️ Low
useIdAdvancedInitialization⭐️ Low–Medium
useSyncExternalStoreAdvancedSubscription⭐️ Low
useInsertionEffectAdvancedBefore layout effects⭐️ Low

💡 Note: Don’t worry if this list feels overwhelming. You’ll rarely use all of them. Start with the basics (useState, useEffect, useContext) and grow from there as your app demands more power or precision.


By classifying Hooks this way, you'll have a mental map for deciding what to use, when—and how it compares to the lifecycle methods you're migrating from. In future articles, we’ll walk you through mapping class lifecycles to function hooks with side-by-side code samples and caveats.

🚀 Putting It All Together

React’s rendering pipeline:

  • JSX → transformed into React elements (objects)

  • Fiber builds a virtual DOM tree and diffs it against the previous tree

  • Commit Phase applies updates to the real DOM and runs side effects

By understanding these internals, you'll be better equipped to migrate class components, create efficient functional components, and visualize how your code interacts with React's rendering lifecycle.🎯 Key Takeaway: Class and function components feed into the same reconciliation engine. Migrating from one to the other means shifting syntax and lifecycle logic, not the rendering model. Know the internals, and your migrations will be precise, elegant, and future-proof.

More from this blog

U

Unclass.dev everything that you need to know about how to migrate from react with classes to functions

5 posts

A practical, step-by-step guide to migrating from React class components to functional components.