From 619e252bfd3c3d005899e867d51e4cfa44ef559a Mon Sep 17 00:00:00 2001 From: snowingfox <1503401882@qq.com> Date: Tue, 11 Aug 2026 16:43:20 +0000 Subject: [PATCH] fix: re-subscribe to ordinary external source after a transition Fixes #2953 --- ...external-source-transition-subscription.md | 5 +++ packages/solid/src/reactive/signal.ts | 10 ++++++ packages/solid/test/external-source.spec.ts | 34 +++++++++++++++++++ 3 files changed, 49 insertions(+) create mode 100644 .changeset/external-source-transition-subscription.md diff --git a/.changeset/external-source-transition-subscription.md b/.changeset/external-source-transition-subscription.md new file mode 100644 index 000000000..25b41632c --- /dev/null +++ b/.changeset/external-source-transition-subscription.md @@ -0,0 +1,5 @@ +--- +"solid-js": patch +--- + +fix: re-establish the ordinary external source subscription after a transition diff --git a/packages/solid/src/reactive/signal.ts b/packages/solid/src/reactive/signal.ts index f4f0309ab..f976cdd3f 100644 --- a/packages/solid/src/reactive/signal.ts +++ b/packages/solid/src/reactive/signal.ts @@ -1486,11 +1486,20 @@ function createComputation( const ordinary = ExternalSourceConfig.factory(sourceFn, trigger); onCleanup(() => ordinary.dispose()); let inTransition: ExternalSource | undefined; + let trackedOrdinary = false; const triggerInTransition: () => void = () => startTransition(trigger).then(() => { if (inTransition) { inTransition.dispose(); inTransition = undefined; + // A computation created while a transition was running only ever + // tracked the transition-scoped source, so its ordinary source has no + // recorded dependencies. Now that the transition is over, re-trigger + // once so the computation runs through `ordinary` again and + // re-subscribes; otherwise it would never receive further external + // updates. If the computation was disposed in the meantime this is a + // no-op because it is no longer an observer of `track`. + if (!trackedOrdinary) trigger(); } }); c.fn = x => { @@ -1500,6 +1509,7 @@ function createComputation( inTransition = ExternalSourceConfig!.factory(sourceFn, triggerInTransition); return inTransition.track(x); } + trackedOrdinary = true; return ordinary.track(x); }; } diff --git a/packages/solid/test/external-source.spec.ts b/packages/solid/test/external-source.spec.ts index 47f407f15..eb8aae1da 100644 --- a/packages/solid/test/external-source.spec.ts +++ b/packages/solid/test/external-source.spec.ts @@ -128,6 +128,40 @@ describe("external source", () => { }); }); + it("should keep receiving external updates after being created during a transition", async () => { + // Initialize SuspenseContext so startTransition creates a real Transition + getSuspenseContext(); + + const flush = () => new Promise(resolve => setTimeout(resolve)); + + await createRoot(async dispose => { + const e = new ExternalSource(0); + let memo: (() => number) | undefined; + + // Create the memo while a transition is running. Its first run tracks + // dependencies on the transition-scoped external source, never on the + // ordinary one. + await startTransition(() => { + memo = createMemo(() => e.get()); + }); + expect(memo!()).toBe(0); + + // First external update is delivered through the transition-scoped source. + e.update(1); + await flush(); + expect(memo!()).toBe(1); + + // Second external update must still be delivered. Before the fix the + // transition-scoped source had been disposed and the ordinary source was + // never subscribed, so this update was silently lost. + e.update(2); + await flush(); + expect(memo!()).toBe(2); + + dispose(); + }); + }); + afterEach(() => { vi.resetModules(); });