Skip to content

Commit d1a566f

Browse files
committed
fix(signal): prevent repeated observer registration in loops
1 parent 7d913a6 commit d1a566f

2 files changed

Lines changed: 61 additions & 35 deletions

File tree

packages/solid/src/reactive/signal.ts

Lines changed: 42 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ export interface SignalState<T> extends SourceMapValue {
8686
value: T;
8787
observers: Computation<any>[] | null;
8888
observerSlots: number[] | null;
89+
lastObserver: Computation<any> | null;
8990
tValue?: T;
9091
comparator?: (prev: T, next: T) => boolean;
9192
// development-only
@@ -157,17 +158,17 @@ export function createRoot<T>(fn: RootFunction<T>, detachedOwner?: typeof Owner)
157158
? { owned: null, cleanups: null, context: null, owner: null }
158159
: UNOWNED
159160
: {
160-
owned: null,
161-
cleanups: null,
162-
context: current ? current.context : null,
163-
owner: current
164-
},
161+
owned: null,
162+
cleanups: null,
163+
context: current ? current.context : null,
164+
owner: current
165+
},
165166
updateFn = unowned
166167
? IS_DEV
167168
? () =>
168-
fn(() => {
169-
throw new Error("Dispose method must be an explicit argument to createRoot function");
170-
})
169+
fn(() => {
170+
throw new Error("Dispose method must be an explicit argument to createRoot function");
171+
})
171172
: fn
172173
: () => fn(() => untrack(() => cleanNode(root)));
173174

@@ -236,6 +237,7 @@ export function createSignal<T>(
236237
value,
237238
observers: null,
238239
observerSlots: null,
240+
lastObserver: null,
239241
comparator: options.equals || undefined
240242
};
241243

@@ -257,7 +259,10 @@ export function createSignal<T>(
257259
return writeSignal(s, value);
258260
};
259261

260-
return [readSignal.bind(s), setter];
262+
const result = [readSignal.bind(s), setter] as Signal<T | undefined>;
263+
if (IS_DEV) (result as any).state = s;
264+
265+
return result;
261266
}
262267

263268
export interface BaseOptions {
@@ -269,7 +274,7 @@ export interface BaseOptions {
269274
// TypeScript Discord conversation: https://discord.com/channels/508357248330760243/508357248330760249/911266491024949328
270275
export type NoInfer<T extends any> = [T][T extends any ? 0 : never];
271276

272-
export interface EffectOptions extends BaseOptions {}
277+
export interface EffectOptions extends BaseOptions { }
273278

274279
// Also similar to OnEffectFunction
275280
export type EffectFunction<Prev, Next extends Prev = Prev> = (v: Prev) => Next;
@@ -386,15 +391,15 @@ export function createEffect<Next, Init>(
386391
export function createReaction(onInvalidate: () => void, options?: EffectOptions) {
387392
let fn: (() => void) | undefined;
388393
const c = createComputation(
389-
() => {
390-
fn ? fn() : untrack(onInvalidate);
391-
fn = undefined;
392-
},
393-
undefined,
394-
false,
395-
0,
396-
IS_DEV ? options : undefined
397-
),
394+
() => {
395+
fn ? fn() : untrack(onInvalidate);
396+
fn = undefined;
397+
},
398+
undefined,
399+
false,
400+
0,
401+
IS_DEV ? options : undefined
402+
),
398403
s = SuspenseContext && useContext(SuspenseContext);
399404
if (s) c.suspense = s;
400405
c.user = true;
@@ -700,15 +705,15 @@ export function createResource<T, S, R>(
700705
initP !== NO_INIT
701706
? (initP as T | Promise<T>)
702707
: untrack(() => {
703-
try {
704-
return fetcher(lookup, {
705-
value: value(),
706-
refetching
707-
});
708-
} catch (fetcherError) {
709-
error = fetcherError;
710-
}
711-
});
708+
try {
709+
return fetcher(lookup, {
710+
value: value(),
711+
refetching
712+
});
713+
} catch (fetcherError) {
714+
error = fetcherError;
715+
}
716+
});
712717
if (error !== undefined) {
713718
loadEnd(pr, undefined, castError(error), lookup);
714719
return;
@@ -909,8 +914,8 @@ export function untrack<T>(fn: Accessor<T>): T {
909914
export type ReturnTypes<T> = T extends readonly Accessor<unknown>[]
910915
? { [K in keyof T]: T[K] extends Accessor<infer I> ? I : never }
911916
: T extends Accessor<infer I>
912-
? I
913-
: never;
917+
? I
918+
: never;
914919

915920
// transforms a tuple to a tuple of accessors in a way that allows generics to be inferred
916921
export type AccessorArray<T> = [...Extract<{ [K in keyof T]: Accessor<T[K]> }, readonly unknown[]>];
@@ -1305,7 +1310,8 @@ export function readSignal(this: SignalState<any> | Memo<any>) {
13051310
Updates = updates;
13061311
}
13071312
}
1308-
if (Listener) {
1313+
if (Listener && this.lastObserver !== Listener) {
1314+
this.lastObserver = Listener;
13091315
const sSlot = this.observers ? this.observers.length : 0;
13101316
if (!Listener.sources) {
13111317
Listener.sources = [this];
@@ -1691,6 +1697,7 @@ function cleanNode(node: Owner) {
16911697
source.observerSlots![index] = s;
16921698
}
16931699
}
1700+
source.lastObserver = null;
16941701
}
16951702
}
16961703

@@ -1771,10 +1778,10 @@ function createProvider(id: symbol, options?: EffectOptions) {
17711778
let res;
17721779
createRenderEffect(
17731780
() =>
1774-
(res = untrack(() => {
1775-
Owner!.context = { ...Owner!.context, [id]: props.value };
1776-
return children(() => props.children);
1777-
})),
1781+
(res = untrack(() => {
1782+
Owner!.context = { ...Owner!.context, [id]: props.value };
1783+
return children(() => props.children);
1784+
})),
17781785
undefined,
17791786
options
17801787
);

packages/solid/test/signals.spec.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -605,6 +605,25 @@ describe("catchError", () => {
605605
expect(errored).toBe(true);
606606
});
607607

608+
describe("Repeatedly read the signal", () => {
609+
test("Signal repeated read in for loop", () => {
610+
const a = createSignal(0);
611+
const dispose = createRoot(dispose => {
612+
createEffect(() => {
613+
for (let i = 0; i < 1000; i++) {
614+
a[0]();
615+
}
616+
});
617+
return dispose;
618+
});
619+
expect((a as any).state.observers.length).toBe(1);
620+
expect((a as any).state.observerSlots.length).toBe(1);
621+
expect((a as any).state.observers[0].sources.length).toBe(1);
622+
expect((a as any).state.observers[0].sourceSlots.length).toBe(1);
623+
dispose();
624+
});
625+
});
626+
608627
test("In nested memo", () => {
609628
let errored = false;
610629
expect(() =>

0 commit comments

Comments
 (0)