Skip to content

Commit 7526ad0

Browse files
authored
Prevent parent signal subscriptions during DOM diff (#979)
1 parent 1e3ab34 commit 7526ad0

3 files changed

Lines changed: 46 additions & 0 deletions

File tree

.changeset/calm-vnodes-listen.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@preact/signals": patch
3+
---
4+
5+
Stop component tracking before Preact reconciles the returned VNode so Signals read by DOM property getters do not subscribe the parent component.

packages/preact/src/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,12 @@ Object.defineProperties(Signal.prototype, {
200200

201201
/** Inject low-level property/attribute bindings for Signals into Preact's diff */
202202
hook(OptionsTypes.DIFF, (old, vnode) => {
203+
// A component's tracking scope ends when its render function returns. The
204+
// next VNode's DIFF hook is the first opportunity to stop tracking before
205+
// Preact begins reconciling the returned VNode.
206+
setCurrentUpdater();
207+
currentComponent = undefined;
208+
203209
if (typeof vnode.type === "string") {
204210
let signalProps: Record<string, any> | undefined;
205211

packages/preact/test/browser/index.test.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,41 @@ describe("@preact/signals", () => {
382382
expect(spy).toHaveBeenCalledOnce();
383383
});
384384

385+
it("should not subscribe to signals read by DOM property getters", () => {
386+
const tag = `x-signal-value-${Math.random().toString(36).slice(2)}`;
387+
388+
class SignalValueElement extends HTMLElement {
389+
internalValue = signal("initial");
390+
391+
get value() {
392+
return this.internalValue.value;
393+
}
394+
395+
set value(value: string) {
396+
this.internalValue.value = value;
397+
}
398+
}
399+
400+
customElements.define(tag, SignalValueElement);
401+
402+
const renderSpy = vi.fn();
403+
function App() {
404+
renderSpy();
405+
return createElement(tag, { value: "initial render" });
406+
}
407+
408+
render(<App />, scratch);
409+
const element = scratch.firstElementChild as SignalValueElement;
410+
expect(element.value).to.equal("initial render");
411+
renderSpy.mockClear();
412+
413+
element.internalValue.value = "user edit";
414+
rerender();
415+
416+
expect(renderSpy).not.toHaveBeenCalled();
417+
expect(element.value).to.equal("user edit");
418+
});
419+
385420
it("should minimize rerenders when passing signals through context", () => {
386421
function spyOn<P = { children?: ComponentChildren }>(
387422
c: FunctionComponent<P>

0 commit comments

Comments
 (0)