Skip to main content

Build Your Own Front-end Framework (React-like)

"React is just a function that takes state and returns a tree of components." — almost true

Building a React-like framework demystifies the abstraction that powers most of the modern web. By the end you understand: the virtual DOM, reconciliation, hooks, fibers, and why React 18 added concurrent rendering. ~500 lines of JavaScript.


1. Overview & motivation​

A React-like framework consists of:

  1. JSX → calls — JSX desugars to createElement(type, props, children). (Or you skip JSX and call h(...) directly.)
  2. Virtual DOM — JS objects representing the desired UI tree.
  3. Reconciliation — diff the new VDOM against the old; produce a minimal patch.
  4. Rendering — apply the patch to real DOM.
  5. Hooks / state — useState, useEffect for components to own state.

What you can only learn by building one:

  • Why the virtual DOM is just a JS object — there's nothing mysterious about it.
  • Why reconciliation is the heart of React (and why React's algorithm has tradeoffs that, e.g., Solid.js rejects).
  • Why hooks have rules ("only call hooks at the top level") — they're stored in an array indexed by call order.
  • Why fiber was introduced — interruptible rendering for responsiveness.

2. Where this fits in the degree​

  • Phase: Production
  • Semester: 8 (System Design and Leadership) or as a capstone elective
  • Modules deepened: Sem 8 Module 1 (system design methodology) — API design forces explicit decisions.

Cross-phase relevance:


3. Prerequisites​

  • JavaScript fluency. Understand closures, hoisting, this, and ES module syntax.
  • Familiarity with React or Vue from the user side.
  • Browser DOM API: createElement, appendChild, setAttribute, event listeners.

4. Theory & research​

Required reading​

  • Rodrigo Pombo, "Build your own React" (pomb.us/build-your-own-react/) — the canonical tutorial. Builds Didact (a React clone) in clear steps. ⭐ recommended primary.
  • Dan Abramov, "Overreacted" blog — overreacted.io. Many posts on React internals by a core contributor.
  • Dan Abramov, "A (Mostly) Complete Guide to React Rendering Behavior" — for understanding what React actually does.
  • React source code — packages/react-reconciler/. Substantial; skim with purpose.
  • Lin Clark, "A Cartoon Intro to React Fiber" (RSJS 2017) — video. Clarifies fiber in 20 minutes.

For component patterns​

  • Sebastian Markbåge, "Algebraic Effects, Fibers, Coroutines" — historical motivation for hooks and fibers.

5. Curated tutorial list (from BYO-X)​

  • JavaScript: WTF is JSX (Let's Build a JSX Renderer)
  • JavaScript: A DIY guide to build your own React
  • JavaScript: Building React From Scratch [video]
  • JavaScript: Gooact: React in 160 lines of JavaScript — github.com/sweetpalma/gooact
  • JavaScript: Learn how React Reconciler package works by building your own lightweight React DOM
  • JavaScript: Build Yourself a Redux
  • JavaScript: Let's Write Redux!
  • JavaScript: Redux: Implementing Store from Scratch [video]
  • JavaScript: Build Your own Simplified AngularJS in 200 Lines of JavaScript
  • JavaScript: Make Your Own AngularJS
  • JavaScript: How to write your own Virtual DOM
  • JavaScript: Building a frontend framework, from scratch, with components (templating, state, VDOM)
  • JavaScript: Build your own React — Rodrigo Pombo, pomb.us ⭐ recommended primary
  • JavaScript: Building a Custom React Renderer [video]

Rodrigo Pombo, "Build your own React".

15 steps, ~150 lines total. Builds Didact:

  1. createElement (JSX-replacement).
  2. render (initial mount).
  3. Concurrent mode (work loop using requestIdleCallback).
  4. Fibers (the data structure).
  5. Render and commit phases.
  6. Reconciliation.
  7. Function components.
  8. Hooks (useState).

This 15-step path is short enough to finish in 1–2 days and covers everything that matters conceptually.

For more depth on Redux: "Let's Write Redux" is similarly short and excellent.

For Angular: Tero Parviainen, "Make Your Own AngularJS" — a full book.


7. Implementation milestones​

Milestone 1: createElement and JSX​

JSX is syntax sugar for createElement calls. Babel transpiles it.

// JSX:
const el = <div id="foo">Hello, world!</div>

// Transpiles to:
const el = createElement("div", { id: "foo" }, "Hello, world!")
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children: children.map(c => typeof c === "object" ? c : createTextElement(c)),
},
};
}
function createTextElement(text) {
return { type: "TEXT_ELEMENT", props: { nodeValue: text, children: [] } };
}

Evidence: createElement('div', null, 'hi') produces the expected JS object.

Milestone 2: render (initial mount)​

Walk the VDOM tree and create real DOM nodes.

function render(element, container) {
const dom = element.type === "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(element.type);
Object.keys(element.props)
.filter(k => k !== "children")
.forEach(k => dom[k] = element.props[k]);
element.props.children.forEach(child => render(child, dom));
container.appendChild(dom);
}

Evidence: render(<App />, document.getElementById('root')) puts the UI on screen.

Milestone 3: Concurrent rendering with fibers​

Break the render into units of work. Each unit handles one fiber. After each unit, check if the browser has more important work (paint, input).

let nextUnitOfWork = null;

function workLoop(deadline) {
let shouldYield = false;
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
shouldYield = deadline.timeRemaining() < 1;
}
requestIdleCallback(workLoop);
}
requestIdleCallback(workLoop);

A fiber is a unit of work — one element + a pointer to its child, sibling, and parent fibers.

Evidence: Rendering 10,000 components doesn't block the main thread. Animations stay smooth.

Milestone 4: Render and commit phases​

Pombo's key insight: separate render (mutate fibers in-memory) from commit (apply to real DOM in one go). Otherwise the user sees half-rendered UI.

function commitRoot() {
commitWork(wipRoot.child);
wipRoot = null;
}
function commitWork(fiber) {
if (!fiber) return;
fiber.parent.dom.appendChild(fiber.dom);
commitWork(fiber.child);
commitWork(fiber.sibling);
}

Milestone 5: Reconciliation​

When state changes, compare new VDOM vs old VDOM. For each fiber:

  • Same type → update props on existing DOM node.
  • Different type → remove old, create new.
  • New element → create.
  • Missing element → delete.
function reconcileChildren(wipFiber, elements) {
// ... walk old and new lists in parallel, tag fibers as UPDATE / PLACEMENT / DELETION
}

The simple reconciliation algorithm: same position + same type = same component. Keys allow you to override this for lists.

Evidence: Editing a text input doesn't reset its focus — reconciliation correctly updates the existing DOM node.

Milestone 6: Function components​

Function components produce VDOM from props:

function App({ name }) {
return <h1>Hello, {name}</h1>;
}

When the framework encounters a function-component fiber, it calls the function with the fiber's props to get its children.

Milestone 7: Hooks​

useState stores state by call order. The framework maintains a per-fiber hook array; each useState call advances an index.

let wipFiber = null;
let hookIndex = null;

function useState(initial) {
const oldHook = wipFiber.alternate?.hooks?.[hookIndex];
const hook = { state: oldHook ? oldHook.state : initial, queue: [] };

const actions = oldHook ? oldHook.queue : [];
actions.forEach(action => { hook.state = action(hook.state); });

const setState = action => {
hook.queue.push(typeof action === "function" ? action : () => action);
wipRoot = { dom: currentRoot.dom, props: currentRoot.props, alternate: currentRoot };
nextUnitOfWork = wipRoot;
deletions = [];
};

wipFiber.hooks.push(hook);
hookIndex++;
return [hook.state, setState];
}

This is literally why hooks must be called in the same order every render. They're stored in an array indexed by call order.

Evidence: Counter component works:

function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}

Milestone 8 (optional): useEffect​

After commit, run side-effect functions. Skip if dependencies haven't changed.

Milestone 9 (optional): Context API​

Pass values through the tree without prop drilling. Each Provider stores a value; useContext looks up the nearest provider.


8. Tests & evidence​

TestHow
Initial mountTree of elements renders correctly
Re-render on state changeCounter increments
Reconciliation preserves identityInput doesn't lose focus on parent re-render
Conditional rendering{condition && <El/>} removes and re-adds correctly
List rendering{items.map(...)} works; keys allow reorder
Hooks rulesCalling useState in a different order throws
Performance1,000-item list re-renders without jank

The strongest evidence: a side-by-side React vs your framework comparison, running the same TodoMVC-style app.


9. Common pitfalls​

  • Forgetting to handle text nodes specially. They're not elements.
  • Mutating fibers during render. Two-pass design (render then commit) avoids this.
  • Hooks called conditionally. Breaks the array index. React's eslint-plugin-react-hooks enforces this rule.
  • Event handlers re-added every render. Solution: track which props are event listeners; remove old, add new.
  • No batching. Multiple setState calls in the same handler should produce one re-render. React 18 batches by default.
  • Ignoring the deletion list. When elements are removed, their DOM nodes must be cleaned up.
  • Not using keys. Without keys, list reordering causes unnecessary DOM thrash.

10. Extensions​

  • Suspense and data fetching — React 18 / 19 territory.
  • Server-side rendering (SSR). Render to HTML strings on the server.
  • Concurrent features — useTransition, useDeferredValue.
  • Custom renderers — react-three-fiber, ink (terminal), react-native.
  • Compiler-based optimization — Solid.js compiles JSX to fine-grained reactive updates with no VDOM.

The path from "tiny React" to "production framework" runs through real-world performance work. React itself has been refined for a decade.


11. Module integration​

ModuleWhat the framework deepens
Sem 8 Module 1 — System designAPI surface design forces decisions you can articulate.
Sem 8 Module 2 — Microservices(Framework lives in the client; less relevant.)
Sem 7 Module 2 — Modular architectureRender and commit phases are textbook separation of concerns.
Interpreter tutorialJSX is a small language. Same parsing techniques.
Compiler tutorialSolid.js's compile-time approach is genuinely compiler work.

12. Portfolio framing​

What to publish:

  • Source organized as src/{element, render, reconcile, hooks}.js.
  • An example app (TodoMVC, Hacker News reader, a small game).
  • A performance benchmark: render 1,000-item list, measure FPS and frame budget.
  • A README with:
    • Architecture diagram (fiber, work loop, render/commit).
    • Supported features.
    • Limitations.

Reviewer entry points:

  • src/render.js — the work loop.
  • src/reconcile.js — the diff.
  • src/hooks.js — the hooks array.
  • demo/todomvc/ — the working app.
  • README must include: feature list, performance numbers, comparison to React on the same demo.

A working React-like framework is an exceptional portfolio piece for front-end engineers. Everyone uses React; almost no one understands its internals.


Source​

This tutorial draws from the BYO-X catalog "Front-end Framework / Library" section. Rodrigo Pombo's "Build your own React" is the canonical primary source.