Back to Blog
Building a React Clone From Scratch: oveReact
frontendreacttypescriptarchitecture

Building a React Clone From Scratch: oveReact

Understanding how React works under the hood by building a custom Virtual DOM, Diffing Engine, and Hooks system in TypeScript.

Building a React Clone From Scratch: oveReact

I recently built oveReact, a custom, lightweight React clone written entirely from scratch in TypeScript. I wanted to deeply understand how React's core concepts work under the hood, stepping away from the "magic" and getting into the nitty-gritty of Virtual DOMs and diffing algorithms.

Unlike simple brute-force clones, oveReact features a true Virtual DOM diffing engine, native HTML DOM property bindings (including Refs), and a robust Hooks system that faithfully mirrors standard React behavior.

The Mental Shift: From Magic to Mechanics

When you first use React, things like useState and useEffect feel magical. But when you build them, you realize it's all just clever closure-based state management and smart DOM manipulation.

Here's a breakdown of what I built:

1. The Virtual DOM Engine

At its core, JSX transpiles down to createElement calls. I built a custom createElement function and a recursive renderNode engine to take these nested objects and convert them into actual DOM nodes.

2. Smart Diffing Algorithm

The most complex part of React is reconciling changes efficiently. My updateElement algorithm compares the old and new Virtual DOM trees, updating only the specific physical DOM nodes that changed. This avoids expensive full-page repaints.

3. Hooks System

Implementing Hooks fundamentally changed how I think about function state:

  • useState: Uses an array to track state across multiple hook calls via a global pointer index, enabling targeted re-renders via a triggerRerender loop.
  • useEffect: Tracks dependency arrays ([]) and utilizes setTimeout to guarantee execution after the DOM paints.
  • useRef: Maintains memory persistence across renders using mutable { current } objects, intercepting the ref prop to attach native DOM nodes.

Dry Run Example

Let's clear up your two big questions first, and then do the complete step-by-step dry run using your actual app.tsx code.

1. How does Counter get executed inside Component()?

In JavaScript, functions can be passed around like regular variables. In app.tsx, you wrote: renderApp(Counter, root);. You are handing the Counter function itself over to renderApp.

In renderer.ts, the function receives it:

export function renderApp(Component: () => VDomElement, container: HTMLElement) {
    // Here, 'Component' is literally just a nickname for 'Counter'
    const newVDom = Component(); // This is EXACTLY the same as writing Counter()
}

Because Component is holding the Counter function, writing Component() executes the code block inside Counter from top to bottom.

2. Why does hookIndex increase if resetHookIndex resets it to 0?

This is about timing. resetHookIndex() is only called ONCE at the very beginning of the render cycle.

  1. renderApp starts.
  2. resetHookIndex() fires -> hookIndex is now 0.
  3. Counter() starts executing top-to-bottom.
    • It hits the 1st useState -> uses index 0, then increments hookIndex to 1.
    • It hits the 2nd useState -> uses index 1, then increments hookIndex to 2.
    • It hits the 1st useEffect -> uses index 2, then increments hookIndex to 3.
  4. Counter() finishes rendering. hookIndex is sitting at 3.

If a user clicks a button and triggers a re-render, renderApp runs again. The very first thing it does is call resetHookIndex(), snapping it back from 3 to 0, so the process can repeat flawlessly!


The Complete Dry Run (Using your app.tsx)

Here is exactly how execution jumps between your files during the lifecycle of your app.

Phase 1: The Initial Boot (First Render)

  1. app.tsx: The browser reads your file and hits renderApp(Counter, root).
  2. renderer.ts: Execution jumps to renderApp.
    • Saves rootContainer = root and rootComponent = Counter.
    • Calls resetHookIndex(). (In hooks.ts, hookIndex = 0).
    • Calls Component(). (Execution jumps back to the Counter function in app.tsx).
  3. app.tsx (Inside Counter):
    • Line 4: const [count, setCount] = useState(0);
      • Jumps to hooks.ts: currentIndex is 0.
      • hooks[0] is empty, so it sets hooks[0] = 0.
      • hookIndex increases to 1.
      • Returns 0 and the setCount function.
    • Line 5: const [text, setText] = useState("Hello");
      • Jumps to hooks.ts: currentIndex is 1.
      • hooks[1] is empty, so it sets hooks[1] = "Hello".
      • hookIndex increases to 2.
      • Returns "Hello" and the setText function.
    • Line 7: useEffect(...)
      • Jumps to hooks.ts: hookIndex is 2.
      • Sets hooks[2] = [count] (saving the dependencies).
      • Schedules the effect to run.
      • hookIndex increases to 3.
    • Line 12: Counter returns the JSX (which transpiles to a VDomElement object).
  4. renderer.ts: We are back! const newVDom now holds that returned object.
    • It sees there is no currentVDom yet, so it calls renderNode(newVDom) to draw it to the physical screen.
    • Saves currentVDom = newVDom. The render is complete.

Phase 2: The Button Click!

A user clicks the <button onClick={() => setCount(count + 1)}>Increment</button>.

  1. hooks.ts: The setCount function runs.
    • Remember, setCount was tied to index 0. It updates hooks[0] = 1.
    • It calls triggerRerender().
  2. renderer.ts: triggerRerender calls renderApp(rootComponent, rootContainer).

Phase 3: The Second Render (Re-render)

  1. renderer.ts: renderApp starts again!
    • Calls resetHookIndex(). (In hooks.ts, hookIndex resets from 3 back down to 0!).
    • Calls Component(). (Execution jumps back to Counter).
  2. app.tsx (Inside Counter):
    • Line 4: const [count, setCount] = useState(0);
      • Jumps to hooks.ts: currentIndex is 0.
      • hooks[0] is NOT empty (it's 1 because of the button click!). It skips initialization.
      • hookIndex increases to 1.
      • Returns 1 and the setCount function.
    • Line 5: const [text, setText] = useState("Hello");
      • Jumps to hooks.ts: currentIndex is 1.
      • hooks[1] is NOT empty (it's "Hello"). It skips initialization.
      • hookIndex increases to 2.
      • Returns "Hello" and setText.
    • Line 7: useEffect(...)
      • Jumps to hooks.ts: hookIndex is 2.
      • It compares the old dependency (hooks[2], which was 0) with the new dependency (count, which is 1).
      • Because 0 !== 1, it knows the dependency changed! It schedules the effect to run again and saves hooks[2] = [1].
      • hookIndex increases to 3.
    • Line 12: Counter returns the newly updated VDomElement (with Count is: 1).
  3. renderer.ts: We are back. const newVDom holds the updated object.
    • This time, currentVDom exists! It calls updateElement(container, newVDom, currentVDom, 0) to figure out exactly what text changed and magically updates just that one tiny piece of the physical screen.

And that is the complete, exact flow of your custom React framework!


Dry Run with useEffect

Let's do a dry run using your exact Counter component from app.tsx!

Here is the setup:

function Counter() {
  const [count, setCount] = useState(0);       // Uses hookIndex 0
  const [text, setText] = useState("Hello");   // Uses hookIndex 1

  useEffect(() => {                            // Uses hookIndex 2
    document.title = `Count is ${count}`;
  }, [count]);

  return <div.../>;
}

Behind the scenes: hooks = []
hookIndex = 0


Phase 1: The First Render

renderApp runs, resets hookIndex = 0, and calls Counter().

  1. useState(0) sets hooks[0] = 0. hookIndex becomes 1.
  2. useState("Hello") sets hooks[1] = "Hello". hookIndex becomes 2.
  3. Execution reaches useEffect:
    • deps passed into the function is [0]. (Because count is currently 0).
    • hookIndex is currently 2.
    • hasNoDeps = false (Because you passed an array).
    • oldDeps = hooks[2]. Since this is the first render, hooks[2] is undefined.
    • depsChanged = true (Because oldDeps is undefined).
    • Because depsChanged is true, the if block executes!
      • It saves hooks[2] = [0]. (We have successfully saved the dependency for next time!)
      • It schedules document.title = "Count is 0" to run via setTimeout.
    • hookIndex increases to 3.

State of the hooks array after 1st render: [0, "Hello", [0]]

(A few milliseconds later, the browser finishes drawing the DOM, and setTimeout fires, updating the document title to "Count is 0".)


Phase 2: The User types in the Text Input!

The user types in the input box, triggering setText("Hello World").

  1. hooks[1] updates to "Hello World".
  2. triggerRerender() runs.

Phase 3: The Second Render (Text changed, but count did not)

renderApp runs, resets hookIndex = 0, and calls Counter().

  1. useState (count) reads hooks[0], returns 0. hookIndex becomes 1.
  2. useState (text) reads hooks[1], returns "Hello World". hookIndex becomes 2.
  3. Execution reaches useEffect:
    • deps passed into the function is still [0]. (Because count is still 0).
    • hookIndex is currently 2.
    • oldDeps = hooks[2], which we saved last time as [0].
    • depsChanged runs .every() to compare the old array [0] against the new array [0].
    • 0 === 0, so depsChanged = false!
    • Because depsChanged is false, the if block is skipped entirely. The effect does NOT run, and setTimeout is ignored.
    • hookIndex increases to 3.

(The effect successfully avoided running because count didn't change!)


Phase 4: The User clicks the Increment Button!

The user clicks the button, triggering setCount(1).

  1. hooks[0] updates to 1.
  2. triggerRerender() runs.

Phase 5: The Third Render (Count changed!)

renderApp runs, resets hookIndex = 0, and calls Counter().

  1. useState (count) reads hooks[0], returns 1. hookIndex becomes 1.
  2. useState (text) reads hooks[1], returns "Hello World". hookIndex becomes 2.
  3. Execution reaches useEffect:
    • deps passed into the function is now [1]. (Because count is now 1).
    • hookIndex is currently 2.
    • oldDeps = hooks[2], which is currently [0].
    • depsChanged runs .every() to compare the old array [0] against the new array [1].
    • 0 !== 1, so .every() fails, meaning depsChanged = true!
    • Because depsChanged is true, the if block executes!
      • It updates hooks[2] = [1]. (Overwriting the old dependency).
      • It schedules document.title = "Count is 1" via setTimeout.
    • hookIndex increases to 3.

Final state of the hooks array: [1, "Hello World", [1]]

(A few milliseconds later, setTimeout fires, updating the document title to "Count is 1".)

Why Build This?

Building a tool from scratch is the ultimate way to understand it. I created a built-in CLI tool (cli.ts) for scaffolding new projects, just like create-react-app. You can run bun cli.ts my-new-app and it instantly spins up a full starter template!

You can check out the source code on GitHub.

Related Posts

Building JavaScript Utilities From Scratch

Building JavaScript Utilities From Scratch

A mental model for frontend utility problems like debounce, throttle, and Promise.all

frontenddevelopmentjavascript+1 more
Read More

Design & Developed by saikatD
© 2026. All rights reserved.