Building a Simple React‑Like Framework: Didact, Fiber Architecture, and useState Implementation
This article explains how to build a lightweight React‑like library called Didact, covering its core concepts, step‑by‑step source‑code walkthrough, implementation of createElement, render, Fiber architecture, concurrent mode, and a useState hook for function components.
Background
As a front‑end developer, studying the source code of the frameworks you use is very helpful, but diving into a framework’s source often brings problems such as the sheer amount of code, high complexity, and outdated tutorial versions.
Too many packages and lines of code, making it hard to start.
Complex code leads to high learning cost.
Articles that guide learning often use outdated framework versions.
How can we solve these problems? Are there good learning materials that can serve as a stepping stone into the mysterious source‑code world?
Introduction
Didact can serve as such a stepping stone. The official Didact website Didact's homepage and its author Rodrigo Pombo describe Didact as:
We are going to rewrite React from scratch. Step by step. Following the architecture from the real React code but without all the optimizations and non‑essential features. Translation: We will rewrite React from the ground up, step by step, following the real React source architecture while omitting optimizations and non‑essential features.
In short, Didact implements a simple React‑like framework.
Therefore we use Didact as a guide to help us understand the real React source code more easily.
It guides us to understand Didact step by step, which aligns with understanding React.
The code is clear and easy to grasp, making it suitable for beginners.
It inherits React’s design philosophy; after React 16.8 the code changed but the ideas stayed the same, so learning Didact helps us grasp React’s concepts.
Core Capabilities
Principles and Source Code Analysis
Starting from a Simple Example
// a JSX element
const element =
Hello
;
// a root container
const container = document.getElementById("root");
// render the element into the container
ReactDOM.render(element, container);This code renders a minimal React application that displays an h1 inside the root container.
JSX works because Babel transforms it into React.createElement(...) (or _jsx(...) ), which is essentially syntactic sugar.
const element = React.createElement(
"h1",
{ title: "foo" },
"Hello"
);To implement a simple React we need to provide createElement and render methods, which we place inside Didact:
function createElement() { /* do sth */ }
function render() { /* do sth */ }
const Didact = { createElement, render };Implementing createElement Method
For an element like:
const element = (
123
Hello
);we need three fields to describe it:
type — h1
props — title="foo"
children — <div>123</div> and Hello
Thus createElement receives type , props , and a variadic children argument, returning a DidactElement . In React the children are usually accessed via const { children } = props; .
type DidactElement = {
type: string | function; // only native and function components in this article
props: { children: DidactElement[]; [key: string]: any };
};
function createElement(type, props, ...children) {
return {
type,
props: { ...props, children: children.map(child =>
typeof child === 'object' ? child : createTextElement(child)
) },
};
}
function createTextElement(text) {
return { type: "TEXT_ELEMENT", props: { nodeValue: text, children: [] } };
}Adding a JSX pragma ( /** @jsx Didact.createElement */ ) lets Babel compile JSX directly to Didact.createElement .
Implementing a Basic render Method
After createElement is ready, we can start building render :
function render(element, container) {
const dom = element.type == "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(element.type);
// set properties
Object.keys(element.props)
.filter(key => key !== "children")
.forEach(name => { dom[name] = element.props[name]; });
// recursively render children
element.props.children.forEach(child => render(child, dom));
container.appendChild(dom);
}This static JSX‑>DOM conversion works, but we will later replace it with a Fiber‑based implementation.
Concurrent Mode (Pre‑knowledge)
In the naive render function the recursion blocks the JS thread, causing long frames and UI jank. The solution is to reserve a slice of each frame for JS work and let the remaining time be used by the GUI thread. The experimental requestIdleCallback API provides the remaining idle time of the current frame.
type Deadline = {
timeRemaining: () => number; // remaining time in the current frame
didTimeout: boolean; // whether the callback timed out
};
function work(deadline) {
if (deadline.timeRemaining() > 1 || deadline.didTimeout) {
// do something
}
requestIdleCallback(work);
}
requestIdleCallback(work);Because the whole recursion is still a single unit, we need to break the work into small units and check the deadline after each unit, turning recursion into a loop.
function work(deadline) {
while (deadline.timeRemaining() > 1 || deadline.didTimeout) {
// perform one unit of work
}
requestIdleCallback(work);
}
requestIdleCallback(work);Fiber Architecture and Two Phases
Modern frameworks like Vue and React use a virtual DOM represented by a tree of JavaScript objects. React implements this with a Fiber tree: an Element tree is first transformed into a Fiber tree, then the Fiber tree is used to update the real DOM.
Scheduler – decides task priority and feeds work to the reconciler.
Reconciler – finds changed components; this is the Render phase.
Renderer – applies changes to the DOM; this is the Commit phase.
Each Fiber node has child (pointing to the first child), sibling (next sibling), and parent pointers. Didact maintains two trees: the current Fiber tree and a work‑in‑progress (WIP) Fiber tree, linked via the alternate pointer.
type Fiber = {
type?: string | function;
props: { children: DidactElement[]; [key: string]: any };
dom: HTMLElement | null;
alternate: Fiber | null;
parent?: Fiber;
child?: Fiber;
sibling?: Fiber;
effectTag?: string;
hooks?: hook[];
};
type hook =
(initialValue?: T | (() => T)) => [T | undefined, (value: T) => T];The work loop drives the construction of the WIP Fiber tree:
let nextUnitOfWork = null;
let currentRoot = null;
let wipRoot = null;
let deletions = null;
function workLoop(deadline) {
let shouldYield = false;
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
shouldYield = deadline.timeRemaining() < 1;
}
if (!nextUnitOfWork && wipRoot) {
commitRoot();
}
requestIdleCallback(workLoop);
}
requestIdleCallback(workLoop);Render Phase
During the Render phase we convert Elements to Fibers. The core function is performUnitOfWork :
function performUnitOfWork(fiber) {
const isFunctionComponent = fiber.type instanceof Function;
if (isFunctionComponent) {
updateFunctionComponent(fiber);
} else {
updateHostComponent(fiber);
}
if (fiber.child) return fiber.child;
let nextFiber = fiber;
while (nextFiber) {
if (nextFiber.sibling) return nextFiber.sibling;
nextFiber = nextFiber.parent;
}
}
function updateHostComponent(fiber) {
if (!fiber.dom) fiber.dom = createDom(fiber);
reconcileChildren(fiber, fiber.props.children);
}
function createDom(fiber) {
const dom = fiber.type == "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(fiber.type);
updateDom(dom, {}, fiber.props);
return dom;
}reconcileChildren builds child Fibers and links them, marking each with an effectTag ("PLACEMENT", "UPDATE", or "DELETION").
function reconcileChildren(wipFiber, elements) {
let index = 0;
let oldFiber = wipFiber.alternate && wipFiber.alternate.child;
let prevSibling = null;
while (index < elements.length || oldFiber != null) {
const element = elements[index];
let newFiber = null;
const sameType = oldFiber && element && element.type == oldFiber.type;
if (sameType) {
newFiber = { type: oldFiber.type, props: element.props, dom: oldFiber.dom, parent: wipFiber, alternate: oldFiber, effectTag: "UPDATE" };
}
if (element && !sameType) {
newFiber = { type: element.type, props: element.props, dom: null, parent: wipFiber, alternate: null, effectTag: "PLACEMENT" };
}
if (oldFiber && !sameType) {
oldFiber.effectTag = "DELETION";
deletions.push(oldFiber);
}
if (oldFiber) oldFiber = oldFiber.sibling;
if (index === 0) {
wipFiber.child = newFiber;
} else if (element) {
prevSibling.sibling = newFiber;
}
prevSibling = newFiber;
index++;
}
}Commit Phase
When the WIP Fiber tree is complete, the Commit phase applies the effects to the real DOM:
function commitRoot() {
deletions.forEach(commitWork);
commitWork(wipRoot.child);
currentRoot = wipRoot;
wipRoot = null;
}
function commitWork(fiber) {
if (!fiber) return;
let domParentFiber = fiber.parent;
while (!domParentFiber.dom) domParentFiber = domParentFiber.parent;
const domParent = domParentFiber.dom;
if (fiber.effectTag === "PLACEMENT" && fiber.dom) {
domParent.appendChild(fiber.dom);
} else if (fiber.effectTag === "UPDATE" && fiber.dom) {
updateDom(fiber.dom, fiber.alternate.props, fiber.props);
} else if (fiber.effectTag === "DELETION") {
commitDeletion(fiber, domParent);
}
commitWork(fiber.child);
commitWork(fiber.sibling);
}
function commitDeletion(fiber, domParent) {
if (fiber.dom) {
domParent.removeChild(fiber.dom);
} else {
commitDeletion(fiber.child, domParent);
}
}Function Component and State Hook
Stateless Function Component
A function component is simply a function that returns elements. During the render phase we execute the function to obtain its children.
let wipFiber = null;
let hookIndex = null;
function updateFunctionComponent(fiber) {
wipFiber = fiber;
hookIndex = 0;
wipFiber.hooks = [];
const children = [fiber.type(fiber.props)];
reconcileChildren(fiber, children);
}Implementing useState
The useState hook stores state and a queue of pending actions. Calling the returned setState pushes an action into the queue and triggers a new render.
function useState(initial) {
const oldHook = wipFiber.alternate && wipFiber.alternate.hooks && 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(action);
wipRoot = { dom: currentRoot.dom, props: currentRoot.props, alternate: currentRoot };
nextUnitOfWork = wipRoot;
deletions = [];
};
wipFiber.hooks.push(hook);
hookIndex++;
return [hook.state, setState];
}Conclusion
We have built a minimal React‑like framework called Didact that supports JSX, a basic render pipeline, Fiber architecture, and a useState hook for function components. The full source code and a demo are available at https://codesandbox.io/s/didact-8-21ost .
Didact is intentionally simple; the real React library adds many optimizations such as top‑level event delegation and Fiber bail‑out mechanisms. Didact follows React’s design ideas to provide a stepping stone for learning the real source code.
References
[1] Didact official site: https://pomb.us/build-your-own-react/
[2] requestIdleCallback documentation: https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestIdleCallback
[3] React source study: https://react.iamkasong.com/
ByteFE
Cutting‑edge tech, article sharing, and practical insights from the ByteDance frontend team.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.