, document.getElementById('root')), the ReactDOM module passes <App />
to the reconciler.
<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.
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.
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.
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:
(string | ReactClass)
Object
An element is not an actual instance. It is a way to tell React what you want to see on the screen.
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.
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.
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.
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.
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.
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()
anduseImperativeHandle()
implies that today functional components also create instances?
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.
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.
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.
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.
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.
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();
a()
function and pushed it onto the execution stack.b()
since it is called inside a()
and pushed it onto the execution stack.b()
function returns, it destroys the context of b()
.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]]
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?
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.
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.
A fiber node represents a stack frame and an instance of a React component. It is comprised of the following:
<div>
and <span>
, for example, for host components (strings).
Classes or functions for composite components.
The same as the key
props passed to the element.
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>
.
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 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
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(...)
oruseMemo(...)
?
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.
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.
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.
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]]
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>
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.
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()
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.
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.
<button>
and causing a state updateUpon 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.
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.
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?
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.
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:
createElement
Function.render
Function.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!
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",
},
};
type
- The type of DOM node we want to create.props
- Object that has all the key:values from the JSX attributes. It also has one special property…children
- In this case a string, but usually an array. Contains the “child” elements. That’s why elements are also trees.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);
createElement
FunctionLet’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.
render
FunctionNext, 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!
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.
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:
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 parent
s 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
!
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.
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.
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:
props
.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.
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.
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:
workLoop
performUnitOfWork
updateFunctionComponent
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:
useEffect
hook.All credit for the source and/or referenced material goes to the named author(s).