Skip to content

Commit 5843e7f

Browse files
fix(component-store): use default equality function for selectSignal (#3884)
1 parent 235f740 commit 5843e7f

File tree

2 files changed

+79
-27
lines changed

2 files changed

+79
-27
lines changed

modules/component-store/spec/component-store.spec.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/* eslint-disable @typescript-eslint/no-non-null-assertion */
22
import {
3+
computed,
34
Inject,
45
Injectable,
56
InjectionToken,
@@ -1500,6 +1501,60 @@ describe('Component Store', () => {
15001501
expect(projectorExecutionCount).toBe(3);
15011502
});
15021503

1504+
it('uses default equality function when equality function is not provided', () => {
1505+
type TestState = { foo: number; bar: { baz: number } };
1506+
1507+
class TestStore extends ComponentStore<TestState> {
1508+
readonly foo = this.selectSignal((s) => s.foo);
1509+
readonly bar = this.selectSignal((s) => s.bar);
1510+
1511+
#fooExecutionCount = 0;
1512+
readonly fooExecutionCount = computed(() => {
1513+
this.foo();
1514+
return ++this.#fooExecutionCount;
1515+
});
1516+
1517+
#barExecutionCount = 0;
1518+
readonly barExecutionCount = computed(() => {
1519+
this.bar();
1520+
return ++this.#barExecutionCount;
1521+
});
1522+
1523+
constructor() {
1524+
super({ foo: 0, bar: { baz: 0 } });
1525+
}
1526+
}
1527+
1528+
const store = new TestStore();
1529+
expect(store.fooExecutionCount()).toBe(1);
1530+
expect(store.barExecutionCount()).toBe(1);
1531+
1532+
store.patchState({ foo: 10 });
1533+
expect(store.fooExecutionCount()).toBe(2);
1534+
// bar should not be executed
1535+
expect(store.barExecutionCount()).toBe(1);
1536+
1537+
store.patchState({ foo: 10 });
1538+
// nothing updated, so execution count should remain the same
1539+
expect(store.fooExecutionCount()).toBe(2);
1540+
expect(store.barExecutionCount()).toBe(1);
1541+
1542+
store.patchState({ bar: { baz: 100 } });
1543+
// foo should not be executed
1544+
expect(store.fooExecutionCount()).toBe(2);
1545+
expect(store.barExecutionCount()).toBe(2);
1546+
1547+
store.patchState(({ bar }) => ({ bar }));
1548+
// nothing updated, so execution count should remain the same
1549+
expect(store.fooExecutionCount()).toBe(2);
1550+
expect(store.barExecutionCount()).toBe(2);
1551+
1552+
store.patchState({ foo: 1000 });
1553+
expect(store.fooExecutionCount()).toBe(3);
1554+
// bar should not be executed
1555+
expect(store.barExecutionCount()).toBe(2);
1556+
});
1557+
15031558
it('throws an error when the signal is read before the state initialization', () => {
15041559
const store = new ComponentStore<{ foo: string }>();
15051560
const foo = store.selectSignal((s) => s.foo);

modules/component-store/src/component-store.ts

Lines changed: 24 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
Signal,
3636
computed,
3737
type ValueEqualityFn,
38+
type CreateComputedOptions,
3839
} from '@angular/core';
3940
import { isOnStateInitDefined, isOnStoreInitDefined } from './lifecycle_hooks';
4041
import { toSignal } from '@angular/core/rxjs-interop';
@@ -341,36 +342,32 @@ export class ComponentStore<T extends object> implements OnDestroy {
341342
options: SelectSignalOptions<unknown>
342343
]
343344
): Signal<unknown> {
344-
if (args.length === 1) {
345-
const projector = args[0] as (state: T) => unknown;
346-
return computed(() => projector(this.state()));
347-
}
348-
349-
const optionsOrProjector = args[args.length - 1] as (
345+
const selectSignalArgs = [...args];
346+
const defaultEqualityFn: ValueEqualityFn<unknown> = (previous, current) =>
347+
previous === current;
348+
349+
const options: CreateComputedOptions<unknown> =
350+
typeof selectSignalArgs[args.length - 1] === 'object'
351+
? {
352+
equal:
353+
(selectSignalArgs.pop() as SelectSignalOptions<unknown>).equal ||
354+
defaultEqualityFn,
355+
}
356+
: { equal: defaultEqualityFn };
357+
const projector = selectSignalArgs.pop() as (
350358
...values: unknown[]
351-
) => unknown | SelectSignalOptions<unknown>;
352-
if (typeof optionsOrProjector === 'function') {
353-
const signals = args.slice(0, -1) as Signal<unknown>[];
354-
355-
return computed(() => {
356-
const values = signals.map((signal) => signal());
357-
return optionsOrProjector(...values);
358-
});
359-
}
359+
) => unknown;
360+
const signals = selectSignalArgs as Signal<unknown>[];
360361

361-
if (args.length === 2) {
362-
const projector = args[0] as (state: T) => unknown;
363-
return computed(() => projector(this.state()), optionsOrProjector);
364-
}
362+
const computation =
363+
signals.length === 0
364+
? () => projector(this.state())
365+
: () => {
366+
const values = signals.map((signal) => signal());
367+
return projector(...values);
368+
};
365369

366-
const signals = args.slice(0, -2) as Signal<unknown>[];
367-
const projector = args[args.length - 2] as (
368-
...values: unknown[]
369-
) => unknown;
370-
return computed(() => {
371-
const values = signals.map((signal) => signal());
372-
return projector(...values);
373-
}, optionsOrProjector);
370+
return computed(computation, options);
374371
}
375372

376373
/**

0 commit comments

Comments
 (0)