Deep Dive into react fiber and render process
Adarsh Somani
Frontend engineer with experience in React, Angular and SSR (Holds canadian PR)
In this chapter, I will try to cover on how react works internally, we all have seen the high level diagrams and definitions of virtual dom, state rendering etc. But can you create your own react like library ?
This article will help you to actually understand that what happens under the hood
So as per above screenshot,
- I am creating a class components, with state object,
- it has a render method with a jsx template containing only an input which increases the counter on key Up
As you can see I am extending it with 'Component' class from react. Component class is imported from react module.
Inside /root/packages/react/src/React.js , you can see React module is exporting the component, PureComponent , createContext etc.
Component Class
If you open the file /root/packages/react/src/ReactBaseClass.js, here you will find the definition of Component class.
Component class has three parameters,
- props - Input Properties
- context - React Context
- Updater Object - Here all the things happenes
Rest of the methods are assigned to prototype of Component function in the same file.
You might be most familiar with setState method. as you can see it takes two inputs:
- PartialState: State values to be updated
- callback function
Both these parameters are later passed to of updater's enqueueSetState method
this.updater.enqueueSetState(this, partialState, callback, 'setState');
But from where does this updater object is prepared and passed to your component function. Where is the enqueueSetState function is defined? Let's try to find the answers inside the codebase of react library.
Updater Object and EnqueueSetState Method
There is a long sequences of processing in react before updater object is attached to a Component.
So we need to check how updater object is prepared behind the curtains.
An root react element should be initialized using 'ReactDOM.render' method
React's magic starts from render method. So React will render the root component and reconciliation of it's children components as well.
Rendering Root Component
Following Steps are used to render the root.
Preparing Root Fiber.
(/root/packages/react-dom/src/client/ReactDOM.js). Root Fiber is of type 'Fiber' , definition of Fiber is declared in /root/packages/react-reconciler/ReactFiber.js. There are many attributes of a 'Fiber' Object, but major attributes to remember are
a. tag: tag is type of WorkTag. This is to determine whether Fiber needs to be created on ClassComponent or HostComponent or Functional Component. There are 21 types of WorkTag as per code (/packages/shared/ReactWorkTags.js)
b. stateNode: It is the internal state of the particular Fiber.
There are several other important attributes of a 'Fiber' class like key, expiration time etc, refer the file /root/packages/react-reconciler/ReactFiber.js
So inside ReactDOM.js - >
So once the root fiber is created , it is assigned to the container component i.e. <App/>
as marked above FiberRoot is different from 'Fiber' as it has few extra properties, most property of our concern is 'current' , which marks root fiber as current.
Next Step is to render complete tree of root component
Render sub tree
Once the root fiber is attached to container component, it calls updateContainer internally. (/root/packages/react-dom/src/client/ReactDOM.js
updateContainer(element, root, parentComponent, work._onCommit);
here root is -> Fiber Root
element-> <App/> - React Node
parentComponent -> null (Since App is our root component)
We have reached till render Root, means we are about to render our root Component (App) in thhe DOM.
As you can see in the diagram we have created a parallel queue over root fiber.
renderRoot will create another loop, in which it will first mount the root component, and then loop on subtree component and reconcile them. During reconciliation of each child component, it will attach a fiber to that, which will be responsible to enqueue the updates.
Please keep in mind that our 'current' fiber is root, once we try to mount other components, we need to change the reference of 'current' to the corresponding fiber.
So we have reached till 'beginWork' method inside ReactFiberBeginWork.js. so we are now going to mount our root component. To recall out App Component has a render function, props, and few lifecycle methods. So beginWork will try to mount the Component Class with these parameters.
'workInProgress' is the fiber on which are working, keep in mind this fiber is not the 'current'. workInProgress.tag is a type of WorkTag, it can be a HostComponent, HostRoot, ClassComponent, Function Component, Lazy Component etc, there is a switch case logic inside beginWork method, which mount the component as per tag type.
it is our 'HostRoot' which we are mounting for the first itme.
so workInProgress.tag = 3 //HostRoot
(/root/packages/react-reconciler/src/ReactFiberBeginWork.js)
I have extended previous diagram a little bit more, your main focus should be on 'workLoop', which is a while loop. So this workLoop calls beginWork method each time and gets a new workInProgress Fiber each time, untill there are no children left.
So In our case once we have rendered our root, our App component will reconcile , and gets a new workInProgress Fiber, Since App Component is class component, our beginWork method will work differently.
You can see in the image, beginWork calls updateClassComponent internally. and our current is marked on root fiber
Since we are rendering the component for the first time , we have created an instance and passed to adoptClassInstance method, which will add 'updater' (Check first few paragraphs of article) to our class component.
If we check classComponentUpdater
So here is our enqueueSetState, which we were finding from start.
One final topic is remaining is how and when the lifecycle methods are called
As you can see after constructCallInstance, mountClassInstance Method is called, when we are rendering for first time. which internally calls componentWillMount, on created component instance
IF we are re-rendering instance, updateClassInstance method is called, which internally class, componentWillReceiveProps,
Once the either mountClassInstance or updateClassInstance are called, finishClassComponent(/packages/react-reconciler/src/ReactFiberBeginWork.js) is called to complete the rendering of component . it calls the render method on class instance 'instance.render()'. which compiles jsx internally.
finishClassComponent also calls componentDidMount or shouldComponentUpdate Lifecycle methods if they are sitting on the fiber.
If given class component has children, then it will run reconciliation on them, and update the 'workInProgress' fiber again to call 'beginWork' method.
This process will be running until all components in the render tree gets reconciled till no children left.
It is a long read, but definitely worth your time, as it helps to know react better, there are many other react methods, which we have not covered like rendering of react-native, rendering of function components etc. But once you understand the flow, rest will be also easier to understand.