All credit for the source and/or referenced material goes to the named author(s).

React's Renderer

#react #fiber

React Fiber Deep Dive on Log Rocket’s Blog

Summary

React, prior to its v16, used a stack-based reconciliation algorithm which enforced sequential, synchronous, operations.

From v16 and on, React has released a new, asynchronous, reconciliation algorithm named React Fiber.

Fiber’s processing model is based on the JavaScript engine’s execution stack, and allows for interuptible, prioritizable, and reusable units of work to be processed.

This helps React reach its scheduling goal of providing a consistently smooth experience while abstracting this complexity from the developers.

What is React Fiber

Fiber’s the name of the new reconciliation algorithm, or reconciler, that’s been implemented in React’s version 16.

Fiber is asynchronous, and as such enables React to:

The previous reconciler was designed with a synchronous stack from which items could be added or removed, but had to work until its stack was empty. Any given task couldn’t be interrupted once it moved to being processed either.

React’s old Stack Reconciler

In ReactDOM.render(<App />, document.getElementById('root')), the ReactDOM module passes <App /> to the reconciler.

What is <App /> ?

It is a React Element, which serves to describe the tree. It is not actually a DOM node, nor an instance of a given component, but simply a way to describe to React what they are.

React abstracts away the complexity of how to build, render, and manage the lifecycle of the actual DOM tree nodes.

Elements Describe the Tree

React Components, Elements, and Instances by Dan Abramov

N.B. Overall, this information is somewhat dated. Elements were replaced with fibers (see below) with the release of React Fiber, and most of that model, while true on the surface in its applicability to the vDOM and the decoupling of in-memory elements and their painted counterparts, is incorrect in its implementation details today. Fibers also work much closer to the actually painted component instances than elements were, in an effort to promote reusability and open optimisation based on the work that’s already been done during previous rendering.

Summary

An element is an immutable plain JavaScript object that describes what you want to appear on the screen in terms of either DOM nodes, or other components. An element can contain other elements. Creating an element is cheap.

A component can be a class or a function. It takees props as an input, and outputs an element tree.

A component only ever receives props when a parent component returned an element with that component as type and these props. Props only flow one way in React; from parent to child.

Instances only apply to class components, not to function components, and are create by React. They are interacted with through Refs.

This seems incorrect, see below for details about refs Hooks. Information is probably outdated.

To create elements, use React.createElement() or JSX.

What is an element?

In React, an element is a plain object describing a component instance or DOM node, and it desired properties. React components return an Element. It holds two fields:

An element is not an actual instance. It is a way to tell React what you want to see on the screen.

DOM Elements

When an element’s type is a string, it represents a DOM node with the corresponding tag name, and props corresponding to its attributes. React elements can be nested to create trees of elements. For instance:

{
	type: 'button',
	props: {
		className: 'button button-blue',
		children: {
			type: 'b',
			props: {
				children: 'OK!'
			}
		}
	}
}

will render into:

<button class="button button-blue">
	<b> OK! </b>
</button>

Those are all just descriptions, not the actual instances. They do not refer to anything on the actual screen. They are just plain objects, and thus they are much lighter than the actual DOM elements.

Component Elements

The type of an element can also be a function or a class corresponding to a React component. For example:

{
	type: Button,
	props: {
		color: 'blue',
		children: 'OK!'
	}
}
An element describing a component is also an element, just like an element describing the DOM node. They can be nested and mixed with each other.

The above means that you can define components as specializations, or extensions, of other components, without worrying about what the extended element renders to in terms of DOM elements.

const DangerButton = ({children} => ({
	type: Button,
	props: {
		color: 'red',
		children: children
	}
}))

You can also mix and match DOM and component elements in a single tree; this helps keep components decoupled from each other, as they can express both “is-a” and “has-a” relationship exclusively through composition.

Components Can Be Classes or Functions

Section omitted as it pertains to class components and makes comments about how function components are stateless where class components aren’t, which isn’t true since Hooks came out.

Components Encapsulate Element Trees

When React reaches and element for which the type is either a function or a class, it “asks” that component what element it renders to, given the provided props.

The above blue button (see [[#Component Elements|component element example]]) will respond with the button DOM element ([[#DOM Elements|see DOM element example]]).

React repeats this process until it knows the underlying DOM tag elements for every component on the page.

The returned element tree can contain both elements describing DOM nodes, and elements describing other components. This lets you compose independent parts of the UI without relying on their internal DOM structure.
Top-Down Reconciliation

This traversal of the tree to resolve elements into their DOM tag from the top is called reconciliation. It starts when ReactDOM.render(), or a state update function such as setState(), gets called.

Unsure how that plays out with Hooks and useState(). This article predates hooks by quite a while.

The output of the reconciliation step is the entire resulting DOM tree. A renderer, such as react-dom or react-native then applies the minimal set of changes necessary to update the UI.

You can also instruct React to skip some parts of the tree if the props haven’t changed, through techniques such as memoization, if some branches become too large to visit efficiently. This gradual process, along with the immutability of elements and their props, is the reason why React can provide optimization with minimal effort.

Not to forget that any diffing and running a VDOM is still overhead, regardless of how efficient it might end up being.

A Word About Instances

Instances aren’t very important in React. Only class components have instances, and those are never created directly by the author.

Refs is the mechanism used to interact with instances, should the need arise.

The existance of the useRef() and useImperativeHandle() implies that today functional components also create instances?

Object-Oriented Programming in React

Section mostly omitted since the content of the above article is more extensive around defining elements and components.

In OOP, the developer needs to manager explicitly the lifecycle of objects based on the state. React abstracts this complexity with elements for which it manages the rendering and lifecycle itself.

What is React Reconciliation?

Section mostly omitted since the content of the above article is more extensive around defining reconciliation.

Reconciliation is the process of walking the tree from the top down to identify every underlying DOM tags that would be rendered.

In the case of a reconciliation triggered by a change in state, React will diff the new tree with the currently rendered tree and then apply the identified changes to the current tree.

What is the React Stack Reconciler?

The stack reconciler from the fact that it uses a LIFO stack to manage tasks. It using a stack is directly tied to the fact that reconciliation performs recursion.

What is Recursion in React?

First, an example of recursion:

(function fibo(n) {
	if (n < 2) {
		return n;
	}
	return fibo(n - 1) + fibo(n - 2);
})(10);

Here, the call stack pushes every call to fibo() on the stack until it pops fibo(1), which is the first function call to return. It then goes back to pushing calls and pops again when it reaches the return statement.

The reconciliation algorithm is itself a purely recursive algorithm. An update results in the entire subtree rerendering immediately. While this works, it has some limitations and downsides.

In a UI context, it’s not necessary for every update to apply immediately; in fact, doing so can be wasteful and cause frame drops. Also to note is that different types of updates have different priorities - an animation update must complete faster than an update from a data store.

Problems with dropped frames & What is Frame Rate

Most of the section omitted due to being known already…

Long story short, you have ~10ms to produce a frame for the browser to paint if you want to provide a smooth experience. If you take longer, you’re “dropping” frames in a sense that some of the needed frames are not being painted.

This is a problem that can occur when the UI updates are being processed by a synchronous, uninteruptible, unprioritized, renderer.

This is where the need for a rewrite arose, and this is where Fiber came to be.

How Does React Fiber Work?

Fiber’s Objectives

Challenges of the JavaScript Execution Stack

Behavior of the Execution Stack

One of the challenges with implementing something like Fiber is how the JavaScript engine works and the lack of threads in the language.

Whenever you write a function in JS, the engine creates a function execution context. The engine will also, on boot, create a global context that holds global objects; for instance window. It then handles both contexts using a stack data structure known as the execution stack.

For instance, given the following code; the engine:

function a() {
	console.log("I am a");
	b();
}

function b() {
	console.log("I am b");
}

a();
  1. Creates a global execution context and pushed it to the execution stack.
  2. Creates a function execution context for the a() function and pushed it onto the execution stack.
  3. Creates a function execution context for b() since it is called inside a() and pushed it onto the execution stack.
  4. When the b() function returns, it destroys the context of b().
  5. When the a() function returns, it destroys the context of a().

Below is a view of the execution stack during execution.

![[A_Deep_Dive_Into_React_Fiber_Execution_Stack.png]]

Treatment of Asynchronous Events

In the case of async events, such as an HTTP Request for instance, the engine does something different than with synchronous items; if it didn’t it would simply block execution while waiting.

On top of the execution stack, the engine has a second data structure; the event queue.

The engine goes to the queue only when the execution stack is empty of nothing but the Global execution context; every time the execution stack empties, the engine checks the event queue and, if applicable, pops items off the queue and handles the event.

The asynchronous part here is in referrence to when they arrive into the queue. They arrive asynchronously, but they aren’t really async in terms of when they are actually handled.

Does the fact that they slide-in in-between sync items mean that they’re not treated asynchronously? If so, what would qualify as truly ascyn, interrupts-like mechanics?

Back to React’s Stack Reconciler

When React traverses the tree for reconciliation purpose, it does so in the execution stack. This means that when an update arrives, they arrive in the event queue (sort of?), which can only be pulled from when the execution stack is empty - effectively meaning that updates have to wait on the current reconciliation being fully processed before being even looked at.

Fiber as an Alernative

The above is exactly what Fiber aims to solve by reimplementing the stack with intelligent capabilities such as pausing, resuming, and aborting.

Fiber is a reimplementation of the stack, specialized for React components. You can think of a single fiber as virtual stack frame. The advantage of this is that you can keep stack frames in memory and execute them however and whenever you want. This is crucial to accomplish React's scheduling goals, and enables potential features such as concurrency and error boundaries. -Andrew Clark

Simply put, a fiber is a unit of work with its own virtual stack. In this implementation, React creates a tree of fiber nodes that can mutate. Those fiber nodes effectively hold the component’s state, props, and underlying DOM element it renders to.

And since fiber nodes are mutable, React doesn’t need to recreate every node for updates; it can simply clone and update the existing node.

In the case of a fiber tree, React doesn’t perform recursive traversal; instead it creates a singly-linked list and performs parent-first, depth-first traversal.

Singly-Linked List of Fiber Nodes

A fiber node represents a stack frame and an instance of a React component. It is comprised of the following:

Type

<div> and <span>, for example, for host components (strings).

Classes or functions for composite components.

Key

The same as the key props passed to the element.

Child

Represents the element returned when render() is called on the component.

const Name = (props) => {
	return <div className="name">{props.name}</div>;
};

For instance, in the example above, the child of <Name> would be <div>.

Sibling

Represents a case where render() returns a list of elements.

const Name = (props) => {
	return [<Customdiv1 />, <Customdiv2 />];
};

In the above example, <Customdiv1> and <Customdiv2> are the children of <Name>, which is the parent. The two children form a singly-linked list.

Is this mutually exclusive with the child property?

Return

Return is the return back to the stack frame, which is a logical return back to the parent fiber node, and thus, to the parent.

pendingProps and memoizedProps

pendingProps represents the props passed to the component. memoizedProps is initialized at the end of the execution stack, storing the props of the given node.

When the incoming pendingProps are equal to memoizedProps, it signals that the previous output can be reused.

Is this baked-in free memoization? How does this play out compared to React.memo(...) or useMemo(...)?

pendingWorkPriority

pendingWorkPriority is a number indicating the priority of the work represented by the fiber.

The smaller the number, the higher the priority.

The scheduler uses the priority to determine which unit of work to perform next.

Alternate

At any given time, a component instance has at most two fibers that correspond to it. That is, the current fiber - what is already rendered - and the in-progress fiber - the stack frame that has not yet returned.

They reference each-other through the alternate property.

Output

The output is the leaf nodes of a React application. The nature of the nodes depends on the execution/rendering context; HTML tags for the web, UI components for React Native, etc.

Conceptually, the output of a fiber is the return value of a function. Every fiber eventually has an output, but the output is created only at the leaf nodes by host components, and then transferred up the tree.

The output is eventually given to the renderer so that it can flush the changes to the rendering environment.

Example

Let’s look at the fiber tree for an app with the below code:

const Parent1 = (props) => {
	return [<Child11 />, <Child12 />];
};

const Parent2 = (props) => {
	return <Child21 />;
};

class App extends Component {
	constructor(props) {
		super(props);
	}

	render() {
		<div>
			<Parent1 />
			<Parent2 />
		</div>;
	}
}

ReactDOM.render(<App />, document.getElementById("root"));

The resulting fiber tree is composed of singly-linked lists of child nodes linkes to each other (sibling relationship) and a linked list of parent-to-child relationships. This tree can be traversed using a depth-first search.

![[A_Deep_Dive_Into_React_Fiber_Example_Fiber_Tree.png]]

Render Phase

The render phase will be analyzed from an example; the source code resides here.

The code basically consists in rendering a <button> with some text content. When clicked, the app destroys the <button> and renders a <div> that contains different text instead. The text is a state variable.

The <App /> has the following structure:

<div>
	<div>
		<button onClick="...">
			<!-- Some Text From State -->
		</button>
	</div>
</div>

Initial Render

As part of the initial render, React creates a “current tree”, and produces the following call stack - captured from a breakpoint in the createFiberFromTypesAndProps() function. This is the function that creates each React fiber from the specific React element.

![[A_Deep_Dive_Into_React_Fiber_Render_Phase_Call_Stack_Initial.png]]

The call stack tracks back to a render() call initially, and all the way up to a createFiberFromTypesAndProps() call. Let’s look at some specific functions in-between these two.

Renderer Functions Deep Dive

workLoopSync()

workLoopSync() is when React starts building up the tree, starting with the <App> node and recursively moving on to <div> , <div> , and finally <button>.

A workInProgress variable holds a reference to the next fiber node that has work to do.

Implementation detail unseen in the trace, unclear on where that is passed around, whether that is a “global” to the renderer, and who the consumer for this reference is …

performUnitOfWork()

performUnitOfWork() takes a fiber node as input, gets the alternate of the node, and calls beginWork().

Supposing the fiber node is the current, the alternate should be the in-progress node. Does that suggest that an alternate always exist and fiber nodes are always created in pairs, or is it created here by copying the current if it has never been created yet?

This is the equivalent of startingthe execution of the function execution context in the execution stack.

beginWork()

When React builds the tree, beginWork() simply leads up to createFiberFromTypeAndProps() and creates the fiber nodes.

React recursively performs work and eventually performUnitOfWork() returns a null, indicating that it has reached the end of the tree.

Clicking the <button> and causing a state update

Upon a state update, React traverses the fiber tree, clones each nodes, and checks whether it needs to perform any work on each of those.

Below is the call stack for this operation:

![[A_Deep_Dive_Into_React_Fiber_Render_Phase_Call_Stack_Rerender.png]]

This trace now includes to new functions, completeUnitOfWork() and completeWork(). Just like performUnitOfWork() and beginWork(), these two functions perform the completion part of the current execution, which means returning back to the stack.

Together, these four functions execute the unit of work and give control over the work being done currently; which is exactly what was missing in the stack reconciler.

A React Fiber Node’s Phases of Work

The below diagram shows how each fiber node is composed of four phases required to complete that unit of work.

![[A_Deep_Dive_Into_React_Fiber_Render_Fiber_Node_Phases.png]]

Important to note here is that a node doesn’t move to completeUnitOfWork() until all its children and sibligs return completeWork().

For instance, it starts with performUnitOfWork() and beginWork() for <App>, then moves on to performUnitOfWork() and beingWork() for <Parent1>, and so on. It comes back and completes the work on <App> once all its children have completed work themselves.

This is when React completes its render phase. The tree produced from the resulting render operation following the state change is called the workInProgress tree. It is, essentially, the draft tree waiting to be rendered.

Commit Phase

The commit phase follows the render phase. It consists in swapping the root pointer of the current tree and the workInProgress tree, effectively swapping the current tree with the tree built up as a result of a state update.

![[A_Deep_Dive_Into_React_Fiber_Commit_Phase.png]]

React also reuses nodes from the “old current tree” after swapping the root pointer from the old to the new tree. This provides smooth transitions from the previous state of the app to the next, and the next, and so on.

Unclear about whether it can keep nodes or versions of the tree around beyond just the old current and the new, for further reuse in the future - do those tree live beyond the transitions?

Scheduling

In order to meet the 10ms maximum frame time, React runs an internal timer for each unit of work being processed, and constantly monitors the time limit.

If the timer runs out, React pauses the current unit of work, hands the control back to the main thread, and lets the browser render whatever is finished at that point.

Wouldn’t this create wonky “partilly updated” paints, even considering that it must render exclusively what is completeWork()? What does that mean if the new UI elements can only be sent down to the rendering context as part of a completed workInProgress tree?

Then, in the next frame, React picks up whre it left off and continues building the tree. Then, when it has enough time, it commits the workInProgress tree and completes the render.

Build your own React by Rodriguo Pombo

Here we’ll be building a React-equivalent renderer based on React 16.8, but that is seemingly still up-to-date.

We’ll cover all the steps in the following order:

  1. The createElement Function.
  2. The render Function.
  3. Concurrent Mode.
  4. Fibers.
  5. Render and Commit Phases.
  6. Reconcilication.
  7. Function Components.
  8. Hooks.

Step Zero: Review

Source example we’ll analyze:

const element = <h1 title="foo">Hello</h1>;
const container = document.getElementById("root");

ReactDOM.render(element, container);

Now let’s remove all React-specific code and replace it with vanilla JS!

Transpiling JSX

const element = <h1 title="foo">Hello</h1>;

Looking at that first line - it contains JSX, which is first compiled into valid JS using tools such as a Babel plugin.

The transformation consists here in transforming this line with a call to React’s createElement function, passing the tag name, the props, and the childrens, as params.

React.createElement creates an object from its arguments. That’s all it does. Here’s the transformed outcome:

const element = React.createElement("h1", { title: "foo" }, "Hello");

And here’s the function call’s output (albeit simplified, a React Element object has more ):

const element = {
	type: "h1",
	props: {
		title: "foo",
		children: "hello",
	},
};

React render

The other piece of React code we need to replace is ReactDOM.render. This is where React changes the DOM, so let’s do that update ourselves.

Building up on our previously transformed code and outputted element:

const node document.createElement(element.type);
node["title"] = element.props.title;

For clarity’s sake, here “element” will refer to React elements and “node” will refer to DOM nodes.

First we create a node using the type from the element, and then assign all props to that node.

Then we create nodes for the children:

const text = document.createTextNode("");
text["nodeValue"] = element.props.children;

Using textNode instead of setting innerText will allow us to treat all elements in the same way later.

Note also that we set nodeValue like we did it with the h1 title - its almost as if the string has props: {nodeValue: "hello"}

Finally, we append the textNode to the h1 and the h1 to the container:

const container = document.getElementById("root");

/* ... Rest of the above code ... */

node.appendChild(text);
container.appendChild(node);

Step One: The createElement Function

Let’s start again with another app. This time we’ll replace the React code with our own version of React!

const element = (
	<div id="foo">
		<a>bar</a>
		<b />
	</div>
);
const container = document.getElementById("root");
ReactDOM.render(element, container);

Let’s start by writing our own version of createElement. Let’s transform the JSX so we can see the createElement calls.

Let’s start with the element itself:

const element = React.createElement(
	"div",
	{ id: "foo" },
	React.createElement("a", null, "bar"),
	React.createElement("b")
);
/* ... */

The only thing that our function needs to do here, is create an element with type and props.

function createElement(type, props, ...children) {
	return (
		type,
		props: {
			...props,
			children,
		},
	)
}

We use the spread operator for the props and the rest parameter syntax for the children, this way the children prop will always be an array.

The children could also contain primitive values, like strings or numbers, so we’ll wrap everything that isn’t an object inside its own element and create a special type for them: TEXT_ELEMENT.

/* ... */
children: children.map((child) =>
	typeof child === "object" ? child : createTextElement(child)
),
	/* ... */

	function createTextElement(text) {
		return {
			type: "TEXT_ELEMENT",
			props: {
				nodeValue: text,
				children: [],
			},
		};
	};
/* ... */

React doesn’t wrap primitive values or create empty arrays when there aren’t children, but we do it because its simpler and we prefer simple to performant here.

Now, we’re still using React’s createElement though. In order to replace it, let’s name our library. We’ll call it Didact.

We still want to be able to use JSX here. How do we tell Babel to use Didact’s createElement instead of React’s?

We can add a special comment like this one:

/** @jsx Didact.createElement */
const element = (
	<div id="foo">
		<a>bar</a>
		<b />
	</div>
);

When Babel transpiles the JSX, it will use the function we define in there.

Step Two: The render Function

Next, we need to write our own version of the ReactDOM.render function.

For now, we only care about adding stuff to the DOM. We’ll handle updating and deleting later.

function render(element, container) {
	// TODO: Create DOM Nodes
}

const Didact = {
	createElement,
	render,
};

/* ... Code from Above Work ... */
Didact.render(element, container);

We start by appending the DOM node using the element’s type, and then append the new node to the container, recursively doing the same for each child.

We also need to handle text elements, and finally assign the element’s props to the node.

function render(element, container) {
	const dom =
		element.type == "TEXT_ELEMENT"
			? document.createTextNode("")
			: document.createElement(element.type);

	Object.keys(element.props)
		.filter((key) => key !== "children")
		.forEach((name) => {
			dom[name] = element.props[name];
		});

	element.props.children.forEach((child) => render(child, dom));

	container.appendChild(dom);
}

And that’s it, we now have a functionning library that can render JSX to the DOM!

Step Three: Concurrent Mode

To note: Concurrent mode isn’t quite a thing in this form in React today, instead being enabled progressively through the use of the framework’s primitives.

Before we start adding more code, we need a refactor…

There’s a problem with our render method’s recursive call. Once we start rendering, we won’t stop until we have rendered the complete element tree. If the element tree is big, it may block the main thread for too long. And if the browser needs to do high priority stuff like handling user input or keeping an animation smooth, it will have to wait until the render finishes.

Understanding is that pre-Fiber React worked this way.

So we are going to break the work into small units, and yield back to the browser after finishing each unit so that it can execute any priority work if need be.

let nextUnitOfWork = null;

function workLoop(deadline) {
	let shouldYield = false;

	while (nextUnitOfWork && !shouldYield) {
		nextUnitOfWork = performUnitOfWork(nextUnitOfWork);

		shouldYield = deadline.timeRemaining() < 1;
	}

	requestIdleCallback(workLoop);
}

function performUnitOfWork(nextUnitOfWork) {
	// TODO
}

We use requestIdleCallback to make a loop. We can think of it as a setTimeout, but instead of us telling it when to run, the browser will run the callback when the main thread is idle.

React doesn’t use requestIdleCallback anymore. Now it uses the scheduler package. But for this use case, it’s conceptually the same.

requestIdleCallback also gives us a deadline parameter. We can use it to check how much time we have until the browser needs to take control again.

To start using the loop, we’ll need to set the first unit of work, and then write a performUnitOfWork function that not only performs the work but also returns the next unit of work.

Step Four: Fibers

To organize the units of work, we’ll need a data structure: a fiber tree.

We’ll have one fiber for each element and each fiber will be a unit of work.

Let’s look at an example:

Didact.render(
	<div>
		<h1>
			<p />
			<a />
		</h1>
		<h2 />
	</div>,
	container
);

In the render we’ll create the root fiber and set it as the nextUnitOfWork. The rest of the work will happen on the performUnitOfWork function. There we will do three things for each fiber:

  1. Add the element to the DOM.
  2. Create the fibers for the element’s children.
  3. Select the next unit of work.

One of the goals of this data structure is to make it easy to find the next unit of work. That’s why each fiber has a link to its first child, its next sibling, and its parent.

![[Build_Our_Own_React_Fiber_Tree_Relationships.png]]

When we finish performing work on a fiber, if it has a child, that fiber will be the next UoW. From our example, when we finish working on the <div> fiber, the next UoW will be the <h1> fiber.

If the fiber doesn’t have a child, we use the sibling as the next UoW instead. For example, the <p> fiber doesn’t have a child, so we move to the <a> fiber after finishing it.

And if the fiber doesn’t have a child nor a sibling we go to the “uncle”: the sibling of the parent. Like <a> and <h2> fibers from the example. Also, if the parent doesn’t have a sibling, we keep going up through the parents until we reach the root. If we have reached the root, it means we have finished performing all the work for this render.

Now, let put all this into code!

We start by extracting part of the logic in render into its own, now fiber-based, function:

function createDom(fiber) {
	const dom =
		fiber.type == "TEXT_ELEMENT"
			? document.createTextNode("")
			: document.createElement(fiber.type);

	Object.keys(fiber.props)
		.filter((key) => key !== "children")
		.forEach((name) => {
			dom[name] = fiber.props[name];
		});

	return dom;
}

function render(element, container) {
	// TODO: Set next UoW
}

let nextUnitOfWork = null;

/* ... */

In the render function we set the nextUnitOfWork to the root of the fiber tree.

function render(element, container) {
	nextUnitOfWork = {
		dom: container,
		props: {
			children: [element],
		},
	};
}

Then, when the browser is ready, it will call our workLoop and wel’ll start working on the root. To do so, it calls the performUnitOfWork function, which we’ll implement now:

First we create a new node and append it to the DOM. We keep track of the DOM node in the fiber.dom property.

Then, for each child, we create a new fiber and we add it to the fiber tree setting it either as a child or as a sibling depending on whether it’s the first child or not.

Finally, we search for the next UoW. We first try with the child, then with the sibling, then with the uncle, and so on.

function performUnitOfWork(fiber) {
	if (!fiber.dom) {
		fiber.dom = createDom(fiber);
	}

	if (fiber.parent) {
		fiber.parent.dom.appendChild(fiber.dom);
	}

	const elements = fiber.props.children;
	let index = 0;
	let prevSibling = null;

	while (index < elements.length) {
		const element = elements[index];

		const newFiber = {
			type: element.type,
			props: element.props,
			parent: fiber,
			dom: null,
		};

		if (index === 0) {
			fiber.child = newFiber;
		} else {
			prevSibling.sibling = newFiber;
		}

		prevSibling = newFiber;
		index++;
	}

	if (fiber.child) {
		return fiber.child;
	}

	let nextFiber = fiber;

	while (nextFiber) {
		if (nextFiber.sibling) {
			return nextFiber.sibling;
		}
		nextFiber = nextFiber.parent;
	}
}

And that’s our performUnitOfWork!

Step Five: Render and Commit Phases

We have a problem here… We are adding a new node to the DOM each time we work on an element, and, let’s remember, the browser could interrupt our work before we finish redering the whole tree. In that case, the user will see a partial UI - and we don’t want that.

So we’ll remove the part that mutates the DOM from performUnitOfWork, that is:

/* ... */
if (fiber.parent) {
	fiber.parent.dom.appendChild(fiber.dom);
}
/* ... */

Instead, we’ll keep track of the root of the fiber tree. We call it the “work in progress” root, or wipRoot.

We change the name the nextUnitOfWork assignation in the render function to wipRoot instead, and assign nextUnitOfWork to initializing it in render. We also declare and initialize it to null in the global scope of the library, right after declaring nextUnitOfWork.

And once we’ve finished all the work - we know it because there isn’t a next unit of work - we commit the whole fiber tree to the DOM. To do so, we add a call to a new commitRoot function in workLoop:

/* ... after while loop ... */
	if (!nextUnitOfWork && wipRoot) {
		commitRoot();
	}

	requestIdleCallback(workLoop);
}

And then we implement commitRoot:

function commitRoot() {
	commitWork(wipRoot.child);
	wipRoot = null;
}

function commitWork(fiber) {
	if (!fiber) {
		return;
	}

	const domParent = fiber.parent.dom;
	domParent.appendChild(fiber.dom);
	commitWork(fiber.child);
	commitWork(fiber.sibling);
}

Here, we recursively append all the nodes to the DOM.

Step Six: Reconciliation

This step is very complex and heavily affects the implementation. To that effect, it is much better seen through the visualizations of the source material

So we only added stuff to the DOM, now we’re going to update and delete.

We need to compare the elements we receive on the render function to the last fiber tree we committed to the DOM.

So we need to save a reference to that “last fiber tree we committed to the DOM” after we finish the commit. We call it currentRoot. We also add the alternate property to every fiber. This property is a link to the old fiber, the fiber that we committed to the DOM in the previous commit phase.

We store the currentRoot as wipRoot in commitRoot. Then we add alternate to every fiber in the initialization code in render, assigning it the currentRoot value. Finally we initialize currentRoot to null along with nextUnitOfWork and wipRoot in the lib’s global scope.

Then we extract the fiber-creating code from performUnitOfWork into a new function - reconcileChildren - which we then reference in performUnitOfWork.

In there, we will reconcile the old fibers with the new elements.

First - the full implementation of reconcileChildren - for a step-by-step see the source material.

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;

		/* Update - reuse the old DOM node */
		if (sameType) {
			newFiber = {
				type: oldFiber.type,
				props: element.props,
				dom: oldFiber.dom,
				parent: wipFiber,
				alternate: oldFiber,
				effectTag: "UPDATE",
			};
		}

		/* New DOM node creation */
		if (element && !sameType) {
			newFiber = {
				type: element.type,
				props: element.props,
				dom: null,
				parent: wipFiber,
				alternate: null,
				effectTag: "PLACEMENT",
			};
		}

		/* Old DOM node removal */
		if (oldFiber && !sameType) {
			oldFiber.effectTag = "DELETION";

			delitions.push(oldFiber);
		}

		if (oldFiber) {
			oldFiber = oldFiber.sibling;
		}

		if (index === 0) {
			fiber.child = newFiber;
		} else {
			prevSiblinb.sibling = newFiber;
		}

		prevSibling = newFiber;
		index++;
	}
}

We iterate at the same time over children of the old fiber (wipFiber.alternate) and the array of elements we want to reconcile.

If we ignore all of the boilerplate needed to iterate over an array and a linked list at the same time, we are left with what matters most inside this while: oldFiber and element. The element is the thing we want to render to the DOM and the oldFiber is what we rendered last time.

We need to compare them to see if there’s any change we need to apply to the DOM.

To compare them, we use the type:

Here, React also uses keys, that makes a better reconciliation. For example, it detects when children change places in the element array.

When the old fiber and the element have the same type, we create a new fiber keeping the DOM node from the old fiber and the props from the element.

We also add a new property to the fiber: the effectTag. We’ll use this property later, during the commit phase.

Then, for the case where the element needs a new DOM node we tag the new fiber with the PLACEMENT effect tag.

And for the case where we need to delete the node, we don’t have a new fiber so we add the effect tag to the old fiber. But when we commit the fiber tree to the DOM, we do it from the WiP Root, which doesn’t have the old fibers… So we need an array to keep track of the nodes we want to remove.

The deletions array is (re-)initialized in the render function, and first declared in the library’s global scope. Then, during a call to commitRoot, we’ll commit the deletion work in those fibers: deletions.forEach(commitWork).

We then update the commitWork function to handle the new effectTags:

function commitWork(fiber) {
	if (!fiber) {
		return;
	}

	const domParent = fiber.parent.dom;

	if (fiber.effectTag === "PLACEMENT" && fiber.dom != null) {
		domParent.appendChild(fiber.dom);
	} else if (fiber.effectTag === "UPDATE" && fiber.dom != null) {
		updateDom(fiber.dom, fiber.alternate.props, fiber.props);
	} else if (fiber.effectTag === "DELETION") {
		domParent.removeChild(fiber.dom);
	}

	commitWork(fiber.child);
	commitWork(fiber.sibling);
}

First, if the fiber has a PLACEMENT effect tag, we do the same as before; append the DOM node to the node from the parent fiber.

If it’s a DELETION, we do the opposite. We remove the child.

And if it’s an UPDATE, we need to update the existing DOM node with the props that changed. We’ll do this in a new function, updateDom:

function updateDom(dom, prevProps, nextProps) {
	// Filtering utilities
	const isEvent = key => key.startsWith("on");
	const isProperty = key => key !== "children" && !isEvent(key);
	const isNew = (prev, next) => key => prev[key] !== next[key];
	const isGone = (prev, next) => key => !(key in next);

	// Remove old or changed event listeners
	Object.keys(prevProps)
		.filter(isEvent)
		.filter(key => !(key in nextProps) || isNew(prevProps, nextProps)(key))
		.forEach(name => {
			const eventType = name.toLowerCase().substring(2);

			dom.removeEventListener(eventType, prevProps[name]);
		});

	// Remove old properties
	Object.keys(prevProps)
		.filter(isProperty)
		.filter(isGone(prevProps, nextProps)))
		.forEach(name => {
			dom[name] = "";
		});

	// Set new or changed properties
	Object.keys(nextProps)
		.filter(isProperty)
		.filter(isNew(prevProps, nextProps))
		.forEach(name => {
			dom[name] = nextProps[name];
		});

	// Add event listeners
	Object.keys(nextProps)
		.filter(isEvent)
		.filter(isNew(prevProps, nextProps))
		.forEach(name => {
			const eventType = name.toLowerCase().substring(2);

			dom.addEventListener(eventType, nextProps[name]);
		});
}

We compare the props from of the old fiber to the props of the old fiber, remove the props that are gone, and set the props that are new or changed.

One special kind of prop that we need to update are event listeners, so if the prop name starts with the “on” prefix we’ll handle them differently. If the event handler changed we remove it from the node, and then we add the new handler.

We can see this full version live, with reconciliation, here.

Step Seven: Function Components

The next thing is support for functional components. First, let’s start from a slightly different example - we’ll use a simple functional component that returns an h1 element.

/** @jsx Didact.createElement */
function App(props) {
	return <h1>Hi {props.name}</h1>;
}

const element = <App name="foo" />;
const container = document.getElementById("root");

Didact.render(element, container);

The transpiled JSX looks like:

function App(props) {
	return Didact.createElement("h1", null, "Hi ", props.name);
}

const element = Didact.createElement(App, {
	name: "foo",
});

Function components are different in two ways:

First we update the performUnitOfWork function:

function performUnitOfWork(fiber) {
	if (fiber.type instanceof Function) {
		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;
	}
}

We check if the fiber type is a function, and depending on that we go to one of two new update function.

In updateHostComponent, we do the same as before:

function updateHostComponent(fiber) {
	if (!fiber.dom) {
		fiber.dom = createDom(fiber);
	}

	reconcileChildren(fiber, fiber.props.children);
}

And in updateFunctionComponent, we run the function to get children.

function updateFunctionComponent(fiber) {
	const children = [fiber.type(fiber.props)];

	reconcileChildren(fiber, children);
}

For our example, ehre the fiber.type is the App function and when we run it, it returns the h1 element. Then, once we have the children, the reconciliation works in the same way, we don’t need to change anything there.

What we need to change is the commitWork function. Now that we have fibers without DOM nodes, we need to change two things (complete resulting function below):

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 != null) {
		domParent.appendChild(fiber.dom);
	} else if (fiber.effectTag === "UPDATE" && fiber.dom != null) {
		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);
	}
}

First, to find the parent of a DOM node, we’ll need to go up the fiber tree until we find a fiber with a DOM node. And when removing a node, we also need to keep going until we find a child with a DOM node.

Step Eight: Hooks

Last step. Now that we have function components, let’s also add state.

/** @jsx Didact.createElement */
function Counter() {
	const [state, setState] = Didact.useState(1);

	return <h1 onClick={() => setState((c) => c + 1)}>Count: {state}</h1>;
}

const element = <Counter />;
const container = document.getElementById("root");

Didact.render(element, container);

Here we’ve got a classic counter component. Each time we click it, it increments the state by one. Note that we’re using Didact.useState to get and update the counter value.

We call the Counter function from updateFunctionComponent’s initial line: const children = [fiber.type(fiber.props)]. It is inside that function that we’ll be calling our new function, useState.

First, we need to initialize some global variables before calling the function component so we can use them inside of the useState function.

let wipFiber = null;
let hookIndex = null;

Now let’s update the updateFunctionComponent function:

function updateFunctionComponent(fiber) {
	wipFiber = fiber;
	hookIndex = 0;
	wipFiber.hooks = [];

	const children = [fiber.type(fiber.props)];

	reconcileChildren(fiber, children);
}

First, we set the work in progress fiber. We also add a hooks array to the fiber to support calling useState several times in the same component, and we keep track of the current hook index.

Now let’s implement useState (full implementation below):

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];
}

When the function component calls useState, we check if we have an old hook. We check in the alternate of the fiber using the hook index.

If we have an old hook, we copy the state from the old hook to the new hook, if we don’t we initialize the state.

Then, we add the new hook to the fiber, increment the hook index by one, and return the state.

useState should also return a function to update the state, so we define a setState function that receives an action (for the Counter example this action is the function that increments the state by one). We push that action to the queue we added to the hook, and then we do something similar to what we did in the render function; set a new work in progress root as the next unit of work so the work loop can start a new rendre phase.

But we haven’t run the action yet. We do it in the next time we are rendering the component. We get all the actions from the old hook queue, and then apply them one by one to the new hook state so when we return the state it’s updated.

And that’s all! We’ve built our own version of React!

We can play with the full version live and find the full source.

Epilogue

Besides helping us understand how React works, one of the goals of this article is to make it easier for us to dive deeper into React’s actual codebase. That’s why Didact’s implementation uses the same variables and functions names as React almost everywhere.

For instance, a function component’s rendering call stack would in React would look like:

We didn’t include a lot of React features and optimizations here; for example, these are a few things that React does differently:

There are also a few features that we could add easily here:

A Cartoon Intro to Fiber by Lin Clark

All credit for the source and/or referenced material goes to the named author(s).