Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changeset/fix-orphan-elements.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"@pionjs/pion": minor
---

fix: prevent orphan elements from rendering

Elements that are constructed but never connected to the DOM (orphan elements) will no longer render or run effects. This fixes issues where lit-html creates elements during template parsing that are never inserted into the DOM.

The scheduler now starts with `_active = false` and only activates when:
- `connectedCallback` is called (for custom elements)
- `resume()` is called explicitly (for virtual components)

Fixes #64
2 changes: 1 addition & 1 deletion src/scheduler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ abstract class BaseScheduler<
this.state = new State(this.update.bind(this), host);
this[phaseSymbol] = null;
this._updateQueued = false;
this._active = true;
this._active = false;
}

update(): void {
Expand Down
1 change: 1 addition & 0 deletions src/virtual.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ function makeVirtual(): Virtual {
teardownOnRemove(this.cont, part);
}
this.cont.args = args;
this.cont.resume();
this.cont.update();
return this.render(...args);
}
Expand Down
37 changes: 37 additions & 0 deletions test/use-effects.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,43 @@ describe("useEffect", () => {
expect(teardowns).to.equal(1, "the effect was not torn down");
});

it("Does not render or run effects for elements that are never connected", async () => {
let renders = 0;
let effects = 0;

function app() {
renders++;

useEffect(() => {
effects++;
return () => {};
}, []);

return html`Test`;
}

customElements.define("orphan-element-test", component(app));

// Create an element but don't connect it to the DOM
const orphan = document.createElement("orphan-element-test") as HTMLElement & { prop: number };
orphan.prop = 1; // Trigger a property setter which would normally queue an update

await nextFrame();

expect(renders).to.equal(0, "the orphan element should not render");
expect(effects).to.equal(0, "the orphan element should not run effects");

// Now connect it
document.body.appendChild(orphan);
await nextFrame();

expect(renders).to.equal(1, "the element should render after being connected");
expect(effects).to.equal(1, "the element should run effects after being connected");

// Cleanup
orphan.remove();
});

it("useEffect(fn, []) runs the effect only once", async () => {
const tag = "empty-array-effect-test";
let calls = 0;
Expand Down