Skip to content

Commit

Permalink
fix(component-store): use default equality function for selectSignal (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
markostanimirovic committed May 9, 2023
1 parent 235f740 commit 5843e7f
Show file tree
Hide file tree
Showing 2 changed files with 79 additions and 27 deletions.
55 changes: 55 additions & 0 deletions modules/component-store/spec/component-store.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import {
computed,
Inject,
Injectable,
InjectionToken,
Expand Down Expand Up @@ -1500,6 +1501,60 @@ describe('Component Store', () => {
expect(projectorExecutionCount).toBe(3);
});

it('uses default equality function when equality function is not provided', () => {
type TestState = { foo: number; bar: { baz: number } };

class TestStore extends ComponentStore<TestState> {
readonly foo = this.selectSignal((s) => s.foo);
readonly bar = this.selectSignal((s) => s.bar);

#fooExecutionCount = 0;
readonly fooExecutionCount = computed(() => {
this.foo();
return ++this.#fooExecutionCount;
});

#barExecutionCount = 0;
readonly barExecutionCount = computed(() => {
this.bar();
return ++this.#barExecutionCount;
});

constructor() {
super({ foo: 0, bar: { baz: 0 } });
}
}

const store = new TestStore();
expect(store.fooExecutionCount()).toBe(1);
expect(store.barExecutionCount()).toBe(1);

store.patchState({ foo: 10 });
expect(store.fooExecutionCount()).toBe(2);
// bar should not be executed
expect(store.barExecutionCount()).toBe(1);

store.patchState({ foo: 10 });
// nothing updated, so execution count should remain the same
expect(store.fooExecutionCount()).toBe(2);
expect(store.barExecutionCount()).toBe(1);

store.patchState({ bar: { baz: 100 } });
// foo should not be executed
expect(store.fooExecutionCount()).toBe(2);
expect(store.barExecutionCount()).toBe(2);

store.patchState(({ bar }) => ({ bar }));
// nothing updated, so execution count should remain the same
expect(store.fooExecutionCount()).toBe(2);
expect(store.barExecutionCount()).toBe(2);

store.patchState({ foo: 1000 });
expect(store.fooExecutionCount()).toBe(3);
// bar should not be executed
expect(store.barExecutionCount()).toBe(2);
});

it('throws an error when the signal is read before the state initialization', () => {
const store = new ComponentStore<{ foo: string }>();
const foo = store.selectSignal((s) => s.foo);
Expand Down
51 changes: 24 additions & 27 deletions modules/component-store/src/component-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
Signal,
computed,
type ValueEqualityFn,
type CreateComputedOptions,
} from '@angular/core';
import { isOnStateInitDefined, isOnStoreInitDefined } from './lifecycle_hooks';
import { toSignal } from '@angular/core/rxjs-interop';
Expand Down Expand Up @@ -341,36 +342,32 @@ export class ComponentStore<T extends object> implements OnDestroy {
options: SelectSignalOptions<unknown>
]
): Signal<unknown> {
if (args.length === 1) {
const projector = args[0] as (state: T) => unknown;
return computed(() => projector(this.state()));
}

const optionsOrProjector = args[args.length - 1] as (
const selectSignalArgs = [...args];
const defaultEqualityFn: ValueEqualityFn<unknown> = (previous, current) =>
previous === current;

const options: CreateComputedOptions<unknown> =
typeof selectSignalArgs[args.length - 1] === 'object'
? {
equal:
(selectSignalArgs.pop() as SelectSignalOptions<unknown>).equal ||
defaultEqualityFn,
}
: { equal: defaultEqualityFn };
const projector = selectSignalArgs.pop() as (
...values: unknown[]
) => unknown | SelectSignalOptions<unknown>;
if (typeof optionsOrProjector === 'function') {
const signals = args.slice(0, -1) as Signal<unknown>[];

return computed(() => {
const values = signals.map((signal) => signal());
return optionsOrProjector(...values);
});
}
) => unknown;
const signals = selectSignalArgs as Signal<unknown>[];

if (args.length === 2) {
const projector = args[0] as (state: T) => unknown;
return computed(() => projector(this.state()), optionsOrProjector);
}
const computation =
signals.length === 0
? () => projector(this.state())
: () => {
const values = signals.map((signal) => signal());
return projector(...values);
};

const signals = args.slice(0, -2) as Signal<unknown>[];
const projector = args[args.length - 2] as (
...values: unknown[]
) => unknown;
return computed(() => {
const values = signals.map((signal) => signal());
return projector(...values);
}, optionsOrProjector);
return computed(computation, options);
}

/**
Expand Down

0 comments on commit 5843e7f

Please sign in to comment.