question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

0.33.7 causes infinite reactivity loops with any custom elements that happen to read a reactive variable in their constructor

See original GitHub issue

Before 0.33.7, things worked fine in my case because reading a reactive variable in a constructor wasn’t triggering reactivity due to the use of cloneNode inside of dom-expression’s get children getter.

Code is similar to this:

@element('my-el')
class MyEl extends Element {
  @attribute foo = "bar" // @attribute creates a signal-backed property

  constructor() {
    this.somethingElseWithInitialValue = this.foo + "baz" // infinite loop, triggers `get children` again
  }
}

A workaround for me for now was to downgrade to 0.33.0. I’ll need to update elements to ensure they don’t read reactive properties in their constructor (easy thing to do, especially in cases where some initial logic depends on them).

Would it make sense to change the compiler output to the following with untrack (or some other way to make the importNode call untracked)?

const _el$123 = untrack(document.importNode(_tmpl$123))

The infinite loop re-runs the children getter repeatedly.

Issue Analytics

  • State:closed
  • Created a year ago
  • Comments:5 (5 by maintainers)

github_iconTop GitHub Comments

1reaction
edemainecommented, Jun 19, 2022

I think we should wrap the importNode call in untrack.

This sounds analogous to createComponent wrapping component functions in untrack. I don’t know WC well enough to say for sure, but it sounds like a good idea.

0reactions
trusktrcommented, Jun 19, 2022

Solid would write before update, basically blocking the prototype on future updates.

This is exactly the problem Custom Element authors need to write robust code against. Custom Element authors need to fix this issue in their custom elements (making them robust against upgrade timing) regardless if Solid side steps the issue in this particular case or not.

The thing about this fix, is that it “fixes” this issue only for people who are writing custom elements and testing them only inside of Solid components.

The moment any other non-Solid app writes values to a pre-upgraded custom element for any reason (it will happen!) Solid’s change to importNode doesn’t come into play at all.

Whoever this fix is for, they need to be aware that they haven’t fixed the problem, and that this issue is only side stepped while they’re testing in Solid.

@element('my-el')
class MyEl extends Element {
  @attribute foo = "bar" // @attribute creates a signal-backed property

  constructor() {
    this.somethingElseWithInitialValue = this.foo + "baz" // infinite loop, triggers `get children` again
  }
}

I’m interested a bit more on an example of how this happens because usually we aren’t cloning(importing) nodes in a reactive context.

I performed the untrack experiment we chatted about in the Discord chat.

First I’ll describe the problem, then show the workaround. This is the problematic code:

@element('my-el')
class MyEl extends Element {
  @attribute foo = {bar: 'baz'} // @attribute creates a signal-backed property, with `{equals: false}`

  constructor() {
    super()

    // Some reactive code that reads and writes to the same signal:
    const foo = this.foo // read .foo
    foo.bar = "someValue"
    this.foo = foo // write .foo
  }
}

This triggers this effect in Solid’s Show component in my case:

function Show(props) {
  let strictEqual = false;
  const condition = createMemo(() => props.when, undefined, {
    equals: (a, b) => strictEqual ? a === b : !a === !b
  });
  return createMemo(() => {
    const c = condition();
    if (c) {
      const child = props.children; // <----------------------------- HERE, calls `importNode`
      return (strictEqual = typeof child === "function" && child.length > 0) ? untrack(() => child(c)) : child;
    }
    return props.fallback;
  });
}

The call to props.children calls importNode which creates a new element, and that element subsequently reads and writes foo within the same memo effect of the Show component.

Each time an element is created, it is a new .foo property with a new signal behind it. So it loops infinitely, although the signal is different on each effect run.

To work around the problem on my end, I can do this:

@element('my-el')
class MyEl extends Element {
  @attribute foo = {bar: 'baz'} // @attribute creates a signal-backed property, with `{equals: false}`

  constructor() {
    super()

    untrack(() => {
      // Some reactive code that reads and writes to the same signal:
      const foo = this.foo // read .foo
      foo.bar = "someValue"
      this.foo = foo // write .foo
    })
  }
}

Now it no longer loops.

I could abstract that workaround into the @reactive decorator like so:

function reactive() {
  return class {
    constructor() {
      return untrack(() => new Class())
    }
  }
}

but with the @element decorator it is much more difficult because we need to avoid the Illegal constructor error from the DOM engine.

best solution:

I think we should wrap the importNode call in untrack. Here’s why:

With DOM APIs custom elements are always constructed in a non-reactive context and do not track any dependencies (because the DOM engine doesn’t use Solid). Therefore I think that Solid should follow the pattern of custom element construction not being reactive with untrack around importNode.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Using custom elements - Web Components | MDN
This is just a simple example, but there is more you can do here. It is possible to define specific lifecycle callbacks inside...
Read more >
Asynchronous assignment operation inside reactivity ... - GitHub
It does indeed seem that any variable assignment happening inside a setTimeout as part of a reactivity statement will cause an infinite loop...
Read more >
Custom Element Best Practices - web.dev
Custom elements let you construct your own HTML tags. This checklist covers best practices to help you build high quality elements.
Read more >
Lifecycle Hooks in Web Components - Ultimate Courses
Custom Element Reactions are called with special care in order to prevent user's code from being executed in the middle of a delicate...
Read more >
Making Web Components reactive - HorusKol
This part is all about making our Web Component reactive, ... One thing that is common to all is how we define our...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found