In the first part of this tutorial, we built a react clone but it did not have React Fiber. This second part of the tutorial will focus on integrating Fiber without breaking any changes for components built using the old version of the library.
What we can learn from Facebook’s React architecture
Check this video out to try and get a better grasp of React Fiber and how we will be implementing the same in our React clone.
Up until this point, our implementation has been made up of recursive calls that perform DOM operations. See reconcile and reconcilechildren . Whenever we make a change in the v-dom we make the corresponding change to the DOM.
With the new architecture, changes to the DOM are made at once when all the updates have been made in a second tree called the work-in-progress tree. The 1st tree is called the current tree and its the tree that represents the nodes in the actual DOM. Once all updates are complete on the work-in-progress tree it gets rendered to the DOM and becomes the current tree.
We’ll re-write the reconciliation algorithm again and this time we’ll use requestIdleCallback. Through this function, the browser lets us know through a callback function how much time is left till it has to perform other tasks in its backlog. Once it’s time for the browser to do other things, we simply pause traversing the tree and resume once the browser has no work to do.
This new re-implementation will rely mostly on looping and not recursion just to make sure that we make the React clone run faster.
The structure of a Fiber
//NB: ALL DOM nodes have their corresponding fibers in our new implementation
// most of this properties will make sense once we begin using them
let fiber = {
tag: HOST_COMPONENT, // we can have either host of class component
type: "input",
parent: parentFiber, // the parentNode’s fiber
child: childFiber, // the childNode’s fiber if it has any
sibling: null, // the element that is in the same tree level as this input
alternate: currentFiber, // the fiber that has been rendered on the dom. Will be null if its on initial render
stateNode: document.createElement(“div”),
props: { children: [], id: "image", type: "text"},
effectTag: PLACEMENT,// can either be PLACEMENT | DELETION | UPDATe depending on the dom operation to be done
effects: [] // this array will contain fibers of its childComponent
};
it’s just a javascript object.
Work Phases
We will have 3 work phases.
The BeginWork phase will traverse the work in progress tree until it gets to the very last child as we apply the effectTag. (we’ll implement this so don’t worry about the details for now)
CompleteWork phase traverses back up the tree until we get to the Fiber with no parent as we apply effects from child to parent until the parent at the top has all the effects of the tree. Effects are simply Fibers that have a tag that will inform the reconciler how to apply the Fiber to the DOM. We can have a tag for adding a node to the DOM, one for updating and another for removing the node.
CommitWork will be responsible for making the DOM manipulations based on the effects array that was created in the CompleteWork phase
Now that we have a general understanding lets begin working on the individual pieces that have will be combined to form the 3 phases described above.
Enough talk lets code. First, clear out the reconciler file with the exception of the imports.
import { updateDomProperties } from "./dom-utils";
import { TEXT_ELEMENT } from "./element";
const ENOUGH_TIME = 1; // we set ours to 1 millisecond.
let workQueue = []; // there is no work initially
let nextUnitOfWork = null; // the nextUnitOfWork is null on initial render.
// the schedule function heere can stand
// for the scheduleUpdate or the
// call to render
// both those calls update the workQueue with a new task.
function schedule(task) {
// add the task to the workqueue. It will be worked on later.
workQueue.push(task);
// request to know when the browser will be pre-occupied.
// if the browser doesn't support requestIdleCallback
// react will pollyfill the function but for simplicities sake
// ill assume your running this on an ever-green browser.
requestIdleCallback(performWork);
}
function performWork(deadline) {
loopThroughWork(deadline)
if (nextUnitOfWork || workQueue.length > 0) {
// if theres more work to be done. get to know when the browser will be occupied
// and check if we can perform some work with the timing provided.
requestIdleCallback(performWork);
}
}
function loopThroughWork(deadline) {
while (nextUnitOfWork && deadline.timeRemaining() > ENOUGH_TIME) {
/**
* perform unitofwork on a fiber if there's enough time to spare
* from the browser's end.
*/
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
}
The schedule function simply updates the workQueue and a call to performWork is made. We don’t really need the schedule function and we’ll replace it in the end since its job will be done by a call to setState and renderit just stands as a placeholder to show you what the 2 functions will do. performWork simply loops through each item in the workQueue and this is how we beginWork
We’ll have a nextUnitOfWork and a performUnitOfWork function. The performUnitOfWork will work on the current-Fiber and return the nextUnitOfWork which will benext Fiber to be worked on.
An update to the workQueue needs to happen when we call setState or render
Let’s begin with making the setState function update the queue.
import { updateDomProperties } from "./dom-utils";
import { TEXT_ELEMENT } from "./element";
const ENOUGH_TIME = 1; // we set ours to 1 millisecond.
let workQueue = []; // there is no work initially
let nextUnitOfWork = null; // the nextUnitOfWork is null on initial render.
// the schedule function heere can stand
// for the scheduleUpdate or the
// call to render
// both those calls update the workQueue with a new task.
function schedule(task) {
// add the task to the workqueue. It will be worked on later.
workQueue.push(task);
// request to know when the browser will be pre-occupied.
// if the browser doesn't support requestIdleCallback
// react will pollyfill the function but for simplicities sake
// ill assume your running this on an ever-green browser.
requestIdleCallback(performWork);
}
function performWork(deadline) {
loopThroughWork(deadline)
if (nextUnitOfWork || workQueue.length > 0) {
// if theres more work to be done. get to know when the browser will be occupied
// and check if we can perform some work with the timing provided.
requestIdleCallback(performWork);
}
}
function loopThroughWork(deadline) {
while (nextUnitOfWork && deadline.timeRemaining() > ENOUGH_TIME) {
/**
* perform unitofwork on a fiber if there's enough time to spare
* from the browser's end.
*/
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
}
const CLASS_COMPONENT = "class";
// ...code
export function scheduleUpdate(instance, partialState) {
workQueue.push({
from: CLASS_COMPONENT, // we know scheduleUpdate came from a class so we have CLASS_COMPONENT here.
instance: instance, // *this* object
partialState: partialState // this represents the state that needs to be changed
});
requestIdleCallback(performWork);
}
view raw
The call to render also needs to update the workQueue
const HOST_ROOT = "root";
const HOST_COMPONENT = "host";
// code..
export function render(elements, containerDom) {
workQueue.push({
from: HOST_ROOT, // the root/parent fiber
dom: containerDom, // document.getElementById("app") just a dom node where this fiber will be appended to as a child
newProps: { children: elements }
});
requestIdleCallback(performWork);
}
Since the nextUnitOfWork is null on initial render, we need a function that gives us our first unit-of-work from the WorkInProgress tree.
function performWork(deadline) {
if (!nextUnitOfWork) {
// on initial render
// or if all work is complete and the nextUnitOfWork is null
//grab the first item on the workInProgress queue.
initialUnitOfWork();
}
loopThroughWork(deadline)
if (nextUnitOfWork || workQueue.length > 0) {
// if theres more work to be done. get to know when the browser will be occupied
// and check if we can perform some work with the timing provided.
requestIdleCallback(performWork);
}
}
function initialUnitOfWork() {
//grab the first item in the array
// its a first come first serve scenario.
const update = workQueue.shift();
// if there are no updates pending
// abort since there is no work to do.
if (!update) {
return;
}
// this call will apply if the update came from setState
// we need the object passed in this.setState to the
// partialState of the current fiber
if (update.partialState) {
update.instance.__fiber.partialState = update.partialState;
}
const root =
update.from === HOST_ROOT
? update.dom._rootContainerFiber
: getRootNode(update.instance.__fiber);
nextUnitOfWork = {
tag: HOST_ROOT,
stateNode: update.dom || root.stateNode, // the properties from the update are checked first for existence
props: update.newProps || root.props, // if the update properties are missing default back to the root properties
alternate: root
};
}
function getRootNode(fiber) {
// climb up the fiber until we reach to the fiber with no parent
// This will give us the alternate property of each fiber if its not
// the host_root, meaning the fiber at the very top of the tree
let node = fiber;
while (node.parent) {
// as long as the current node has a parent keep climbing up
// until node.parent is null.
node = node.parent;
}
return node;
}
Let’s now define our performUnitOfWork function.
This function performs work on the Fiber that has been passed to it as a parameter. It then goes ahead to work on its children and finally works on its siblings and the cycle continues.
Small visual representation of how the work is done.
// ... code
function performUnitOfWork(wipFiber) {
// lets work on the fiber
beginWork(wipFiber);
if (wipFiber.child) {
// if a child exists its passed on as
// the nextUnitOfWork
return wipFiber.child;
}
// No child, we call completeWork until we find a sibling
let uow = wipFiber;
while (uow) {
completeWork(uow); // completework on the currentFiber
// return the siblings of the currentFiber to
// be the nextUnitOfWork
if (uow.sibling) {
// Sibling needs to beginWork
return uow.sibling;
}
// if no siblings are present,
// lets climb up the tree as we call completeWork
// when no parent is found / if we've reached the top,
// this function returns null and thats how we know that we have completed
// working on the work in progress tree.
uow = uow.parent;
}
}
The idea is, go down the tree traversing the siblings and children, then come back up completing work on each level. (We go down then come back up again)
Let’s define beginWork
// ...code
function beginWork(wipFiber) {
if (wipFiber.tag == CLASS_COMPONENT) {
updateClassFiber(wipFiber);
} else {
updateHostFiber(wipFiber);
}
}
function updateHostFiber(wipFiber) {
if (!wipFiber.stateNode) {
// if this is the initialRender and stateNode is null
// create a new node.
wipFiber.stateNode = createDomElement(wipFiber);
}
const newChildElements = wipFiber.props.children;
reconcileChildrenArray(wipFiber, newChildElements);
}
function updateClassFiber(wipFiber) {
let instance = wipFiber.stateNode;
if (instance == null) {
// if this is the initialRender call the constructor
instance = wipFiber.stateNode = createInstance(wipFiber);
} else if (wipFiber.props == instance.props && !wipFiber.partialState) {
// nothing has changed here
// lets move to the children
cloneChildFibers(wipFiber);
return;
}
instance.props = wipFiber.props;
instance.state = Object.assign({}, instance.state, wipFiber.partialState);
wipFiber.partialState = null;
const newChildElements = wipFiber.stateNode.render();
reconcileChildrenArray(wipFiber, newChildElements);
}
function createInstance(fiber) {
//similar to the previous implementation
// we instanciate a new object of the class provided in the
// type prop and return the new instance
const instance = new fiber.type(fiber.props);
instance.__fiber = fiber;
return instance;
}
function createDomElement(fiber) {
// check the type of the fiber object.
const isTextElement = fiber.type === TEXT_ELEMENT;
const dom = isTextElement
? document.createTextNode("")
: document.createElement(fiber.type);
updateDomProperties(dom, [], fiber.props);
return dom;
}
We perform different operations for the various Fibers. Either class or host component
Since we have reconciled the host Fibers, we also need to reconcile their children as well.
// .. code
const PLACEMENT = "PLACEMENT"; // this is for a child that needs to be added
const DELETION = "DELETION"; //for a child that needs to be deleted.
const UPDATE = "UPDATE"; // for a child that needs to be updated. refresh the props
function createArrayOfChildren(children) {
// we can pass children as an array now in the call to render
/**
* render () {
return [
<div>First</div>,
<div>Second</div>
]
}
*/
return !children ? [] : Array.isArray(children) ? children : [children];
}
function reconcileChildrenArray(wipFiber, newChildElements) {
const elements = createArrayOfChildren(newChildElements);
let index = 0;
// let the oldFiber point to the fiber thats been rendered in the
// dom if its present. if its initialRender then return null.
let oldFiber = wipFiber.alternate ? wipFiber.alternate.child : null;
let newFiber = null;
while (index < elements.length || oldFiber != null) {
const prevFiber = newFiber;
// we wither get an element or false back in this check.
const element = index < elements.length && elements[index];
// if the type of the old fiber is the same as the new fiber
// we just need to update this fiber
// its the same check as the one we had in the previous
// reconciliation algorithm
const sameType = oldFiber && element && element.type == oldFiber.type;
if (sameType) {
// on an update the only new thing that gets
// changed is the props of the fiber
// I should have spread this but for easier
// understading and so that we understand where everything
// goes and the underlying structure, Ill do what seemengly seems
//like im repeating myself.
newFiber = {
type: oldFiber.type,
tag: oldFiber.tag,
stateNode: oldFiber.stateNode,
props: element.props,
parent: wipFiber,
alternate: oldFiber,
partialState: oldFiber.partialState,
effectTag: UPDATE
};
}
if (element && !sameType) {
// this is when an element wasn't present
// before but is now present.
newFiber = {
type: element.type,
tag:
typeof element.type === "string" ? HOST_COMPONENT : CLASS_COMPONENT,
props: element.props,
parent: wipFiber,
effectTag: PLACEMENT
};
}
if (oldFiber && !sameType) {
// in this check we see its when a component
// was present, but is now not present.
// like a deleted to do list.
oldFiber.effectTag = DELETION;
wipFiber.effects = wipFiber.effects || [];
// we need to keep a reference of what gets deleted
// here we add the fiber to be deleted onto the effects array.
// we'll work with the effects later on in the commit stages.
wipFiber.effects.push(oldFiber);
}
if (oldFiber) {
// we are only interested in the siblings of the
// children that are in the same level here
// tree level here
// in other terms we just need the siblings of the render array.
oldFiber = oldFiber.sibling;
}
if (index == 0) {
wipFiber.child = newFiber;
} else if (prevFiber && element) {
prevFiber.sibling = newFiber;
}
index++;
}
}
We need to keep track of the children that need to be updated, deleted or appended as new components
we made a call to cloneChildFibers, in essence, the function gives the children of the parentFiber a new parent property. The parentFiber from the work in progress tree becomes their new parent. This is to replace the currentFiber of the node that has been rendered to the DOM. Let’s define it below
// .. code
function cloneChildFibers(parentFiber) {
const oldFiber = parentFiber.alternate;
// if there is no child for the alternate
// there's no more work to do
// so just kill the execution
if (!oldFiber.child) {
return;
}
let oldChild = oldFiber.child;
// on initial render, the prevChild is null.
let prevChild = null;
/**
* below we are essencially looping through all the siblings
* so that can give them their new parent which is the workInProgress fiber
* the other properties are hard coded as well.
* I could have spread them but for understanding of the
* structure given, We are not going to spread them here.
*/
while (oldChild) {
const newChild = {
type: oldChild.type,
tag: oldChild.tag,
stateNode: oldChild.stateNode,
props: oldChild.props,
partialState: oldChild.partialState,
alternate: oldChild,
parent: parentFiber
};
if (prevChild) {
prevChild.sibling = newChild;
} else {
parentFiber.child = newChild;
}
prevChild = newChild;
oldChild = oldChild.sibling;
}
}
Now that we have cloned all the children and there’s nothing else left for us to do. It’s finally time to complete the work done and finally flush the changes to the DOM.
// ...code
let pendingCommit = null; // this is what will be flushed to the dom
// ... code
function completeWork(fiber) {
// this function takes the list of effects of the children and appends them to the effects of
// the parent
if (fiber.tag == CLASS_COMPONENT) {
// update the stateNode.__fiber of the
// class component to the new wipFiber (it doesn't deserve this name anymore since we are done with the work we needed to do to it)
fiber.stateNode.__fiber = fiber;
}
if (fiber.parent) {
// append the fiber's child effects to the parent of the fiber
// the effects of the childFiber
// are appended to the fiber.effects
const childEffects = fiber.effects || [];
// if the effectTag is not present of this fiber, if there are none,
// then return an empty list
const thisEffect = fiber.effectTag != null ? [fiber] : [];
const parentEffects = fiber.parent.effects || [];
// the new parent effects consists of this current fiber's effects +
// effects of this current Fiber + the parent's own effects
fiber.parent.effects = parentEffects.concat(childEffects, thisEffect);
} else {
// if the fiber does not have a parent then it means we
// are at the root. and ready to flush the changes to the dom.
pendingCommit = fiber;
}
}
completeWork.
completeWork simply takes a Fiber and appends it’s own effects to the Fiber’s parent. It also takes a step further to append the effects of the Fiber’s child to the parent of the current Fiber. Once there is no parent Fiber then it means that we have reached the very top of our tree and now set pendingCommit to the Fiber with no parent.
Keep in mind these effects are what we will use to determine the kind of operation we need to apply to the Fiber on the DOM.
We now need a way to tell the performWork function to finally flush the changes to the DOM-based off of thependingCommit
function performWork(deadline) {
// ...code
if (pendingCommit) {
commitAllWork(pendingCommit);
}
}
function commitAllWork(fiber) {
// this fiber has all the effects of the entire tree
fiber.effects.forEach(f => {
commitWork(f);
});
// the wipFiber becomes the currentFiber
fiber.stateNode._rootContainerFiber = fiber;
nextUnitOfWork = null; // no work is left to be done
pendingCommit = null; // we have just flushed the changes to the dom.
}
function commitWork(fiber) {
if (fiber.tag == HOST_ROOT) {
return;
}
let domParentFiber = fiber.parent;
while (domParentFiber.tag == CLASS_COMPONENT) {
domParentFiber = domParentFiber.parent;
}
const domParent = domParentFiber.stateNode;
if (fiber.effectTag == PLACEMENT && fiber.tag == HOST_COMPONENT) {
// add the new element to the dom
domParent.appendChild(fiber.stateNode);
} else if (fiber.effectTag == UPDATE) {
// update the dom properties of the element.
updateDomProperties(fiber.stateNode, fiber.alternate.props, fiber.props);
} else if (fiber.effectTag == DELETION) {
// remove the node from the DOM its not needed
commitDeletion(fiber, domParent);
}
}
function commitDeletion(fiber, domParent) {
// this function
// removes the siblings of the current fiber
// if a sibling is not present jump back to the parent
// of the fiber. This is if the node is not equal to the fiber
let node = fiber;
while (true) {
if (node.tag == CLASS_COMPONENT) {
// check the child of the class component.
// then loop back.
node = node.child;
continue;
}
domParent.removeChild(node.stateNode);
while (node != fiber && !node.sibling) {
// if there are no siblings jump back up to
// to the node's parent.
node = node.parent;
}
if (node == fiber) {
return;
}
node = node.sibling;
}
}
commitAllWork runs through all effects calling commitWork on each one. Commit work then either make a call to commitDeletionin case of deletion or adds the stateNode to the DOM in case of placement or simply updates the DOM properties in case of an update. This is all determined by the effectTagon the fiber.
We are done now .
It’s testing time.
Build the module
$ yarn run build:all
Open up an example file you have in your example folder and see everything still works.
For a full-blown app
Follow along with this repository’s setup to see how you can make a demo app with Webpack and babel using your React clone.
Don’t use this module in production either.
Main Take-Aways
The core React team went a long way to optimize the React codebase to be as fast for your apps as possible.
This frees up the main thread to handle things like animations and makes your apps even more responsive.
2. Have fewer divs and wrappers.
Each wrapper is a Fiber, the more they are the more time it to reconcile your updates and flush the changes to the DOM. Use fragments to avoid having many unnecessary nodes and fibers that have to be traversed for little to no reason.
Fragments
We couldn’t fit adding fragments into this tutorial but what React does, when it identifies a fragment in the tree, it skips over it and goes to its children. Here is an example in the React codebase of how React deals with updating fragments
3. Use Hooks.
Hooks help reduce the heavy nesting of your components that are introduced by higher-order components and render props. If there is too much nesting your apps become slower.
If you follow any of the above 3 steps /all of them, React and the main thread will not have to do as much work to give your users a seamless user experience.
Hooks help reduce the heavy nesting of your components that are introduced by higher-order components and render props. If there is too much nesting your apps become slower.
If you follow any of the above 3 steps /all of them, React and the main thread will not have to do as much work to give your users a seamless user experience.
References:
This video provides a basic understanding of modern React