Understanding The React Source Code - Initial Rendering (Simple Component) II

In the last article we went through the process that creates a ReactCompositeComponent instance from JSX expression. This one will continue the walk-through of simple component rendering, from the point of batchedMountComponentIntoNode().

Files used in this article:

renderers/dom/client/ReactMount.js: defines mountComponentIntoNode(), entry point of the logic process in this article, and ReactDOMContainerInfo which is used in DOM rendering

renderers/shared/stack/reconciler/ReactReconciler.js: call mountComponent of various ReactXXXComponent

renderers/shared/stack/reconciler/ReactCompositeComponent.js: call mountComponent to initiate TopLevelWrapper, and performInitialMount to initiate ReactDOMComponent

renderers/dom/shared/ReactDOMComponent.js: define ReactDOMComponent

A complete static call hierarchy used in this article:

1
2
3
4
5
6
7
8
9
10
11
12
13
|=ReactMount.render(nextElement, container, callback)     ___
|=ReactMount._renderSubtreeIntoContainer() |
|-ReactMount._renderNewRootComponent() |
|-instantiateReactComponent() |
|~batchedMountComponentIntoNode() upper half
|~mountComponentIntoNode() (platform agnostic)
|-ReactReconciler.mountComponent() |
|-ReactCompositeComponent.mountComponent() |
|-ReactCompositeComponent.performInitialMount() |
|-instantiateReactComponent() _|_
|-ReactDOMComponent.mountComponent() lower half
|-_mountImageIntoNode() (HTML DOM specific)
_|_

batchedMountComponentIntoNode() does not do much. It simply invokes another function call to mountComponentIntoNode().

For now let’s omit the delicacy of those indirect function calls and just see them as direct ones. I will cover transaction and batched updates in later articles.

mountComponentIntoNode() - the cross point of the two halves

mountComponentIntoNode() is the cross point of logic that consists of platform agnostic code (in this article referred as “upper half”) and logic that is HTML specific (lower half). All major tasks of this walk-through are also effectively completed within this function (and its sub-calls), from where 1) a ReactDOMComponent is derived from the top level ReactCompositeComponent[T]; 2) the ReactDOMComponent is rendered to a real DOM element, and 3) the element is inserted into the document object.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function mountComponentIntoNode(
wrapperInstance, // scr: -----> ReactCompositeComponent[T]
container, // scr: -----> document.getElementById("root")
transaction, // scr: -----> not of interest
shouldReuseMarkup, // scr: -----> null
context, // scr: -----> emptyObject
) {
...
var markup = ReactReconciler.mountComponent( // scr: -----> 1),2)
wrapperInstance,
transaction,
null,
ReactDOMContainerInfo(wrapperInstance, container),
context,
0 /* parentDebugID */,
);
...
ReactMount._mountImageIntoNode( // scr: -----> 3)
markup,
container,
wrapperInstance,
shouldReuseMarkup,
transaction,
);

ReactMount@renderers/dom/client/ReactMount.js

In which, 1) is still conducted as a high level operation in upper half, while 2), 3) are concrete DOM operations. After 2) completes, we will be able to see the element <h1 style={"color":"blue"}>hello world</h1> rendered on the screen.

For 1), ReactReconciler.mountComponent() is yet another simple function that calls the corresponding mountComponent() of internalInstance passed to it, in this case, ReactCompositeComponent[T].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
mountComponent: function(
internalInstance,
transaction,
hostParent,
hostContainerInfo,
context,
parentDebugID, // 0 in production and for roots
) {
var markup = internalInstance.mountComponent(
transaction,
hostParent,
hostContainerInfo,
context,
parentDebugID,
);
... // scr: transaction related code
return markup;
},

ReactReconciler@renderers/shared/stack/reconciler/ReactReconciler.js

One special parameter here is ReactDOMContainerInfo, it is constructed at the same time when it is passed down to ReactReconciler.mountComponent():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function ReactDOMContainerInfo(topLevelWrapper, node) {
var info = {
_topLevelWrapper: topLevelWrapper, // scr: -------------------> ReactCompositeComponent[T]
_idCounter: 1,
_ownerDocument: node ? node.nodeType === DOC_NODE_TYPE ? node : node.ownerDocument : null, // scr: -----> node.nowerDocument
_node: node, // src: -----> document.getElementById("root")
_tag: node ? node.nodeName.toLowerCase() : null, // scr: -----> 'div'
_namespaceURI: node ? node.namespaceURI : null // scr: ----->
element.namespaceURI
};
... // scr: DEV code
return info;
}

ReactDOMContainerInfo@renderers/dom/client/ReactMount.js

The result object ReactDOMContainerInfo[ins] will be used by 3).

Here is the end of the corridor that consists of transient functions. Now we move on to the next important stop.

ReactCompositeComponent.mountComponent() - initialize ReactCompositeComponent[T]

This is where the magic, I mean, the reaction happens

In the previous step only _currentElement of ReactCompositeComponent[T] is populated with a reference to ReactElement[2], which makes the object a little bit dull. But not anymore. This property will in turn be used to trigger a “reaction” within ReactCompositeComponent[T] and transforms (initialize) the object to something more meaningful.

The designated data structure of this step is:

The call stack in action is:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
ReactDOM.render
|=ReactMount.render(nextElement, container, callback)
|=ReactMount._renderSubtreeIntoContainer()
|-ReactMount._renderNewRootComponent()
|-instantiateReactComponent()
|~batchedMountComponentIntoNode(
componentInstance, // scr: -----> ReactCompositeComponent[T]
container, // scr: -> document.getElementById("root")
shouldReuseMarkup, // scr: -----> null
context, // scr: -----> emptyObject
)
|~mountComponentIntoNode(
wrapperInstance, // scr: -----> ReactCompositeComponent[T]
container, // scr: -----> same
transaction, // scr: -----> not of interest
shouldReuseMarkup, // scr: ---> same
context, // scr: -----> not of interest
)
|-ReactReconciler.mountComponent(
internalInstance, // scr: --> ReactCompositeComponent[T]
transaction, // scr: --> not of interest
hostParent, // scr: --> null
hostContainerInfo,// scr: --> ReactDOMContainerInfo[ins]
context, // scr: --> not of interest
parentDebugID, // scr: --> 0
)
/* we are here */
|-ReactCompositeComponent[T].mountComponent(same)

Next let’s look at the ReactCompositeComponent.mountComponent() implementation.

I will not list the implementation for small functions (when they are get called) but give return value directly for the sake of concision.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
mountComponent: function(
transaction,
hostParent,
hostContainerInfo,
context,
// scr: this ------> ReactCompositeComponent[T]
) {
// scr: --------------------------------------------------------> 1)
this._context = context; // scr: -----> emptyObject
this._mountOrder = nextMountID++; // scr: ----------------------> global veriable, accumulative
this._hostParent = hostParent; // scr: -----> null
this._hostContainerInfo = hostContainerInfo; // scr: -----------> ReactDOMContainerInfo[ins]
var publicProps = this._currentElement.props; // scr: ----------> { child: ReactElement[1] }
var publicContext = this._processContext(context); // scr: -----> meaning less, emptyObject
// scr: --------------------------------------------------------> 2)
var Component = this._currentElement.type; // scr: -------------> TopLevelWrapper
var updateQueue = transaction.getUpdateQueue(); // scr: --------> not of interest
// Initialize the public class
var doConstruct = shouldConstruct(Component); // scr: ----------> true, for TopLevelWrapper.prototype.isReactComponent = {};
var inst = this._constructComponent(
doConstruct,
publicProps,
publicContext,
updateQueue,
); // scr: ----------> call TopLevelWrapper’s constructor
var renderedElement;
// Support functional components
if (!doConstruct && (inst == null || inst.render == null)) {
...
} else {
if (isPureComponent(Component)) { // scr: --------------------> TopLevelWrapper.prototype.isPureReactComponent is not defined
...
} else {
this._compositeType = CompositeTypes.ImpureClass;
}
}
// scr: --------------------------------------------------------> 3)
// These should be set up in the constructor, but as a convenience
// for simpler class abstractions, we set them up after the fact.
inst.props = publicProps; // scr: ----> { child: ReactElement[1] }
...
// scr: --------------------------------------------------------> 4)
this._instance = inst; // scr: ---------------------------------> link the ReactCompositeComponent[T] to the TopLevelWrapper instance
// Store a reference from the instance back to the internal representation
ReactInstanceMap.set(inst, this); // scr: ----------------------> link the TopLevelWrapper instance back to ReactCompositeComponent[T]
...
var markup;
if (inst.unstable_handleError) { // scr: -----------------------> false, TopLevelWrapper.prototype.unstable_handleError is not defined
...
} else {
// scr: --------------------------------------------------------> 5)
markup = this.performInitialMount( // scr: a initial at the end?
renderedElement,
hostParent,
hostContainerInfo,
transaction,
context,
);
}
...
return markup;
}

ReactCompositeComponent@renderers/shared/stack/reconciler/ReactCompositeComponent.js

Running in the context of ReactCompositeComponent[T] (as a member function), this method

1) assigns the parameters directly to the corresponding properties of ReactCompositeComponent[T] and local variables. The interesting one variable here is publicProps. It will be used very soon;

2) extracts TopLevelWrapper from this._currentElement.type, and calls its constructor to create a TopLevelWrapper instance. In this process:

1
shouldConstruct(Component)

checks if TopLevelWrapper.prototype.isReactComponent is set, and returns true if so; and

1
this._constructComponent()

calls the TopLevelWrapper constructor if true is the result of the previous function. I will name the instance TopLevelWrapper[ins] this time.;

This is the time you may want to check the definition of TopLevelWrapper in the previous post, search for ***.

3) initializes the TopLevelWrapper[ins].props of the new instance with the information stored previously in the wrapper element ReactElement[2] through the publicProps in 1);

4) creates a doubly link between this (ReactCompositeComponent[T]) and TopLevelWrapper[ins]. One link is created using this._instance, another is made with ReactInstanceMap. this._instance will be used very soon in the next step, and ReactInstanceMap will be used in later posts.

I type 4 stars **** here as you might need to come back to check ReactInstanceMap‘s origin.

5) goes to the next step.

ReactCompositeComponent.performInitialMount() - create a ReactDOMComponent from ReactElement[1]

This step strips the wrapper and creates a ReactDOMComponent instance. Plus, we are going to see ReactHostComponent the tirst time. This component is used to chain the function call from the upper half to lower half.

First thing first, the target data structure:

The call stack in action:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ReactDOM.render
|=ReactMount.render(nextElement, container, callback)
|=ReactMount._renderSubtreeIntoContainer()
|-ReactMount._renderNewRootComponent()
|-instantiateReactComponent()
|~batchedMountComponentIntoNode()
|~mountComponentIntoNode()
|-ReactReconciler.mountComponent()
|-ReactCompositeComponent[T].mountComponent(same)
/* we are here */
|-ReactCompositeComponent[T].performInitialMount(
renderedElement, // scr: -------> undefined
hostParent, // scr: -------> null
hostContainerInfo, // scr: -------> ReactDOMContainerInfo[ins]
transaction, // scr: -------> not of interest
context, // scr: -------> not of interest
)

ReactCompositeComponent.performInitialMount() does three things. It 1) extracts the ReactElement[1] as mentioned before; 2) instantiates a ReactDOMComponent based on the ReactElement[1].type; and 3) calls ReactDOMComponent.mountComponent() to render a DOM element.

why a performInitialMount is invoked at the end of the mountComponent function? Because it is “initial” for the next mountComponent

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
performInitialMount: function(
renderedElement,
hostParent,
hostContainerInfo,
transaction,
context,
) {
var inst = this._instance;
var debugID = 0;
if (inst.componentWillMount) { // scr: ----------> undefined

}
// scr: ------------------------------------------------------> 1)
// If not a stateless component, we now render
if (renderedElement === undefined) {
renderedElement = this._renderValidatedComponent(); // scr: ---> calls TopLevelWrapper.render() to extract ReactElement[1].
}
// scr: ------------------------------------------------------> 2)
var nodeType = ReactNodeTypes.getType(renderedElement); // scr: -> ReactNodeTypes.HOST
this._renderedNodeType = nodeType;
var child = this._instantiateReactComponent(
renderedElement,
nodeType !== ReactNodeTypes.EMPTY /* shouldHaveDebugID */,
);
this._renderedComponent = child;
// scr: ------------------------------------------------------> 3)
var markup = ReactReconciler.mountComponent(
child,
transaction,
hostParent,
hostContainerInfo,
this._processChildContext(context),
debugID,
);
return markup;
},

ReactCompositeComponent@renderers/shared/stack/reconciler/ReactCompositeComponent.js

Now I give a detailed explanation for each step:

1) this._renderValidatedComponent() simply calls TopLevelWrapper.render() so that ReactElement[1] extracted from TopLevelWrapper[T].props.child is assigned to renderedElement.

2) this._instantiateReactComponent() is an alias to _instantiateReactComponent@renderers/shared/stack/reconciler/instantiateReactComponent.js that has been discussed in the last post. However, renderedElement(i.e., ReactElement[1]) is used to create a ReactDOMComponent instance this time…

wait here,

if we look at the implementation of _instantiateReactComponent() again

1
2
3
4
5
6
7
8
...
// Special case string values
if (typeof element.type === ‘string’) { // scr: -------> this time
instance = ReactHostComponent.createInternalComponent(element);
}
...

_instantiateReactComponent@renderers/shared/stack/reconciler/instantiateReactComponent.js

the createInternalComponent() of ReactHostComponent is got called because ReactElement[1].type is “h1”, which instantiate a genericComponentClass:

1
2
3
4
5
6
function createInternalComponent(element) {
...
return new genericComponentClass(element);
}

ReactHostComponent@renderers/shared/stack/reconciler/ReactHostComponent.js

but why it has anything to do with ReactDOMComponent?

In fact, the platform specific component ReactDOMComponent is “injected” to ReactHostComponent as genericComponentClass during compiling time. For now we consider link has already been established and injection mechanism will be discussed later posts.

I type *5 here.

The constructor of ReactDOMComponent is very similar to that of ReactCompositeComponent:

1
2
3
4
5
6
7
8
9
function ReactDOMComponent(element) {
var tag = element.type; // scr: --------> 'h1'
...
this._currentElement = element; // scr: --------> ReactElement[1]
this._tag = tag.toLowerCase(); // scr: --------> 'h1'
... // scr: default values, null, 0, etc.
}

ReactDOMComponent@renderers/dom/shared/ReactDOMComponent.js

We name the returned instance ReactDOMComponent[ins];

3) ReactReconciler.mountComponent() has already been covered, it simply calls the mountComponent() of the first parameter, in this case, ReactDOMComponent[ins].

Now the logic processes to lower half.

To be continued…

That's it. Did I make a serious mistake? or miss out on anything important? Or you simply like the read. Link me on -- I'd be chuffed to hear your feedback.