Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/external-source-transition-subscription.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"solid-js": patch
---

fix: re-establish the ordinary external source subscription after a transition
10 changes: 10 additions & 0 deletions packages/solid/src/reactive/signal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1486,11 +1486,20 @@ function createComputation<Next, Init = unknown>(
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 => {
Expand All @@ -1500,6 +1509,7 @@ function createComputation<Next, Init = unknown>(
inTransition = ExternalSourceConfig!.factory(sourceFn, triggerInTransition);
return inTransition.track(x);
}
trackedOrdinary = true;
return ordinary.track(x);
};
}
Expand Down
34 changes: 34 additions & 0 deletions packages/solid/test/external-source.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
Expand Down