Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save obuchtala/a51d66158d039e979e1650ae562e6f24 to your computer and use it in GitHub Desktop.

Select an option

Save obuchtala/a51d66158d039e979e1650ae562e6f24 to your computer and use it in GitHub Desktop.

Understanding Component.js stack-traces

With the new Component.js API it is now easier to debug errors in the render stack. Still it needs a basic understanding of how things are working.

There is an inherent problem with the approach of Component.js that makes it quite challenging to create a transparent debug experience. Component.js lets you compose VirtualElements a-priori, pass them around as props, and append them to a parent element at a later time. The context of definition, the owner component, is often different to the parent component.

Think of the following situation:


function A() {
  A.super.apply(this, arguments);

  this.render = function($$) {
    var el = $$('div');
    var grandchildren = [
      $$(C, { name: 'foo' }).ref('foo'),
      $$(C, { name: 'bar' }).ref('bar')
    ];
    el.append(
      $$(Child).append(grandchildren)
    );
    return el;
  };
}
Component.extend(A);

function B() {
  B.super.apply(this, arguments);

  this.render = function($$) {
    return $$('div').append(this.props.children);
  };
}
Component.extend(B);

function C() {
  C.super.apply(this, arguments);

  this.render = function($$) {
    return $$('div').append(this.props.name);
  };
}
Component.extend(C);

When A#render() is called, grandchildren are created as VirtualComponents owned by the A instance. Their actual parent is not known, without running B#render().

B is created as a VirtualComponent, too, passing grandchildren via props. In this case it is known that A is the parent, thus, the virtual B component can be turned into a real B instance, and B#render() can be evaluated.

After that, the parent of grandchildren foo and bar is known and they can be instantiated, and C#render() can be evaluated for each of them.

During this recursive descent which we call capturing phase, VirtualComponents are turned into real Component instances and their render() method is called. The overall result is still a virtual representation of a DOM, with real Component instances attached.

Stacktrace considerations

The trace of function calls for such a recursion could look like this:

- _capture
  - a = new A()
  - a.render()
  - _descend()
    - b = new B(a, {...})
    - b.render()
    - _descend()
      - foo = new C(b, {...})
      - foo.render()
      - bar = new C(b, {...})
      - bar.render()

However, this stack makes debugging difficult to understand. Consider an exception during foo.render(). The stack-trace would then look like

- foo.render() << uncaught Error
- _descend
- _descent
- _capture

This means there is no back-link to the parents' render() calls, although, the root of the error is likely to be found there.

To improve this, the render() calls need to be used as a root for descending. Considering the incremental composition possibilities this can not be done without a multi-pass strategy:

  • first pass: necessary to declare virtual components
  • second pass and more:
    • turn VirtualComponent into real Component
    • call render() recursively

I.e., only the first render() is used to create VirtualComponents. The subsequent calls are used to walk $$() statements and trigger recursive rendering if possible. This creates more meaningful positions within the render() method in the stack-trace.

Example:

function Parent(parent, props) {
  Parent.super.apply(this, arguments)

  this.render = function($$) {
    var el = $$('div');
    el.append($$(Child));
    return el;
  }
}

After the first pass, we know that there is a virtual Child component. When $$(Child) is executed during the second pass, we actually trigger the recursion.

- _capture
  - p = new Parent()
  - p.render()
  - p.render()
    - $$(...)
      - child = new Child(p, {...})
      - child.render()

If an error occurs during Child#render() the stack-trace looks like:

- child.render() << uncaught Error
- $$(...)
- p.render()
- _capture

Now there is a back-link to the render() call of the parent component, though, it is the second pass, not the first.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment