How does React update a component and its children after a state change?

I am watching Paul O Shannessy - Building React From Scratch

And I understand the mounting process very well but I have hard day trying to understand how React update a component and its children

The reconciler controls the update process by this method:

function receiveComponent(component, element) {
  let prevElement = component._currentElement;
  if (prevElement === element) {
    return;
  }

  component.receiveComponent(element);
}

Component.receiveComponent

 receiveComponent(nextElement) {
    this.updateComponent(this._currentElement, nextElement);
  }

and this is the Component.updateComponent method:

  updateComponent(prevElement, nextElement) {
    if (prevElement !== nextElement) {
      // React would call componentWillReceiveProps here
    }

    // React would call componentWillUpdate here

    // Update instance data
    this._currentElement = nextElement;
    this.props = nextElement.props;
    this.state = this._pendingState;
    this._pendingState = null;

    let prevRenderedElement = this._renderedComponent._currentElement;
    let nextRenderedElement = this.render();
 
    if (shouldUpdateComponent(prevRenderedElement, nextRenderedElement)) {
      Reconciler.receiveComponent(this._renderedComponent, nextRenderedElement);
    } 
  }

This is the part of the code that updates the component after state change, and i assume that it should update the children too, but i can't understand how this code achieves that, in the mounting process React instantiate components to dive deeper in the tree but this doesn't happen here, we need to find the first HTML element then we can change our strategy and update that HTML element in another place in the code, and I can't find any way to find any HTML elements this way.

Finding the first HTML is the way to stop this endless recursion and logically this is what I expect from the code, to stop recursion the same way in the mounting process, but in mounting, this demanded component instantiation so we can delegate to the reconciler that will discover that we are dealing with a wrapper instance of an HTML element not a wrapper instance of a custom component then React can place that HTML element in the DOM.

I can't understand how the code works in the update process. this code as I see won't dive deeper in the tree and I think won't update the children and can't let React find the first HTML element so React can update the DOM element, isn't it?

This is the code repo on Github


Solution 1:

I created a codesandbox to dig in

Here is the codesandbox I created

and here's a short recording of me opening the debugger and seeing the call stack.

How it works

Starting from where you left off, Component.updateComponent:

  updateComponent(prevElement, nextElement) {
  //...
    if (shouldUpdateComponent(prevRenderedElement, nextRenderedElement)) {
      Reconciler.receiveComponent(this._renderedComponent, nextRenderedElement);
  //...

in the Component.updateComponent method Reconciler.receiveComponent is called which calls component.receiveComponent(element);

Now, this component refers to this._renderedComponent and is not an instance of Component but of DOMComponentWrapper

and here's the receiveComponent method of DOMComponentWrapper:

  receiveComponent(nextElement) {
    this.updateComponent(this._currentElement, nextElement);
  }

  updateComponent(prevElement, nextElement) {
    // debugger;
    this._currentElement = nextElement;
    this._updateDOMProperties(prevElement.props, nextElement.props);
    this._updateDOMChildren(prevElement.props, nextElement.props);
  }

Then _updateDOMChildren ends up calling the children render method.

here's a call stack from the codesandbox I created to dig in.

call stack from setState until child render

How do we end up in DOMComponentWrapper

in the Component's mountComponent method we have:

let renderedComponent = instantiateComponent(renderedElement);
this._renderedComponent = renderedComponent;

and in instantiateComponent we have:

  let type = element.type;

  let wrapperInstance;
  if (typeof type === 'string') {
    wrapperInstance = HostComponent.construct(element);
  } else if (typeof type === 'function') {
    wrapperInstance = new element.type(element.props);
    wrapperInstance._construct(element);
  } else if (typeof element === 'string' || typeof element === 'number') {
    wrapperInstance = HostComponent.constructTextComponent(element);
  }

  return wrapperInstance;

HostComponent is being injected with DOMComponentWrapper in dilithium.js main file:

HostComponent.inject(DOMComponentWrapper);

HostComponent is only a kind of proxy meant to invert control and allow different Hosts in React.

here's the inject method:

function inject(impl) {
  implementation = impl;
}

and the construct method:

function construct(element) {
  assert(implementation);

  return new implementation(element);
}