Skip to content

Commit 7b11c8d

Browse files
committed
Clarify deferred subscription replay
Assisted-By: devx/e0620a8d-8624-4748-b9c7-ce390238fc37
1 parent f42dd40 commit 7b11c8d

3 files changed

Lines changed: 26 additions & 5 deletions

File tree

mangle.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,9 @@
6565
"$_signal": "s",
6666
"$_updater": "__$u",
6767
"$_updateFlags": "__$f",
68-
"$_updaters": "U"
68+
"$_updaters": "U",
69+
"$_subscribers": "u",
70+
"$_usage": "o"
6971
}
7072
}
7173
}

packages/react/runtime/src/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,8 @@ function startComponentEffect(
121121
this: Signal,
122122
node: SubscriptionNode
123123
) {
124+
// Only defer this effect's direct dependencies. Computed dependencies are
125+
// subscribed recursively when their direct node is committed.
124126
if ((node as { _target: Effect })._target === nextStore.effect) {
125127
nextStore._subscribers.push({ signal: this, node });
126128
}
@@ -331,7 +333,6 @@ function createEffectStore(
331333
},
332334
[symDispose]() {
333335
this.f();
334-
this._subscribers = [];
335336
},
336337
};
337338

@@ -408,10 +409,12 @@ export function _useSignalsImplementation(
408409
if (_usage === UNMANAGED) useIsomorphicLayoutEffect(cleanupTrailingStore);
409410

410411
useIsomorphicLayoutEffect(() => {
412+
// Detach first because subscribing can synchronously invoke user callbacks.
411413
const subscribers = store._subscribers;
412414
store._subscribers = [];
413415

414416
subscribers.forEach(({ signal, node }) => {
417+
// Nodes omitted from the committed render are marked with version -1.
415418
if (
416419
(node as { _target: Effect })._target === store.effect &&
417420
(node as { _version: number })._version !== -1

packages/react/runtime/test/browser/subscription-lifecycle.test.tsx

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -202,22 +202,31 @@ describe("subscription lifecycle", () => {
202202
expect(getTargets(tracked)).to.equal(undefined);
203203
});
204204

205-
it("restores the subscribe implementation active before rendering", async () => {
205+
it("restores and invokes the subscribe implementation active before rendering", async () => {
206+
const tracked = signal(0);
207+
let wrapperCallCount = 0;
206208
function subscribeWrapper(
207209
this: Signal,
208210
node: Parameters<Signal["_subscribe"]>[0]
209211
) {
212+
wrapperCallCount++;
210213
return originalSubscribe.call(this, node);
211214
}
212215
Signal.prototype._subscribe = subscribeWrapper;
213-
const pending = new Promise<void>(() => {});
216+
let resolve!: () => void;
217+
let resolved = false;
218+
const pending = new Promise<void>(r => {
219+
resolve = r;
220+
});
214221
let restoredWrapper = false;
215222

216223
/** @noUseSignals */
217224
function App(): JSX.Element {
218225
const store = useSignals(1);
219226
try {
220-
throw pending;
227+
const value = tracked.value;
228+
if (!resolved) throw pending;
229+
return <p>{value}</p>;
221230
} finally {
222231
store.f();
223232
}
@@ -232,11 +241,18 @@ describe("subscription lifecycle", () => {
232241
);
233242
});
234243
restoredWrapper = Signal.prototype._subscribe === subscribeWrapper;
244+
245+
resolved = true;
246+
await act(async () => {
247+
resolve();
248+
await pending;
249+
});
235250
} finally {
236251
Signal.prototype._subscribe = originalSubscribe;
237252
}
238253

239254
expect(restoredWrapper).to.equal(true);
255+
expect(wrapperCallCount).to.equal(1);
240256
});
241257

242258
it("restores the prototype before rendering an error boundary", async () => {

0 commit comments

Comments
 (0)