From 0ed9f472579450b6709385b2c51423ac8ed97760 Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Tue, 25 Apr 2023 13:09:18 -0500 Subject: [PATCH] refactor: add internal implementation of toSignal for State observable --- .../store/spec/integration_signals.spec.ts | 4 +-- modules/store/src/store.ts | 12 ++++---- modules/store/src/to_signal.ts | 28 +++++++++++++++++++ .../auth/containers/login-page.component.ts | 2 +- .../src/app/core/containers/app.component.ts | 2 +- 5 files changed, 39 insertions(+), 9 deletions(-) create mode 100644 modules/store/src/to_signal.ts diff --git a/modules/store/spec/integration_signals.spec.ts b/modules/store/spec/integration_signals.spec.ts index 8eed6a92ca..776727b333 100644 --- a/modules/store/spec/integration_signals.spec.ts +++ b/modules/store/spec/integration_signals.spec.ts @@ -118,7 +118,7 @@ describe('NgRx and Signals Integration spec', () => { }); describe('context integration spec', () => { - it('Store.selectSignal should throw an error if used outside in the injection context', () => { + it('Store.selectSignal should not throw an error if used outside in the injection context', () => { let error; try { @@ -127,7 +127,7 @@ describe('NgRx and Signals Integration spec', () => { error = `${e}`; } - expect(error).toContain('Error: NG0203'); + expect(error).toBeUndefined(); }); }); }); diff --git a/modules/store/src/store.ts b/modules/store/src/store.ts index ef5f1ddbb0..097f76f722 100644 --- a/modules/store/src/store.ts +++ b/modules/store/src/store.ts @@ -1,6 +1,5 @@ // disabled because we have lowercase generics for `select` -import { Injectable, Provider, Signal } from '@angular/core'; -import { toSignal } from '@angular/core/rxjs-interop'; +import { computed, Injectable, Provider, Signal } from '@angular/core'; import { Observable, Observer, Operator } from 'rxjs'; import { distinctUntilChanged, map, pluck } from 'rxjs/operators'; @@ -8,12 +7,15 @@ import { ActionsSubject } from './actions_subject'; import { Action, ActionReducer, FunctionIsNotAllowed } from './models'; import { ReducerManager } from './reducer_manager'; import { StateObservable } from './state'; +import { toSignal } from './to_signal'; @Injectable() export class Store extends Observable implements Observer { + private readonly state: Signal; + constructor( state$: StateObservable, private actionsObserver: ActionsSubject, @@ -22,6 +24,7 @@ export class Store super(); this.source = state$; + this.state = toSignal(state$); } select(mapFn: (state: T) => K): Observable; @@ -100,9 +103,8 @@ export class Store * * @param selectorFn selector function */ - selectSignal(selectorFn: (state: T) => K): Signal; - selectSignal(selectorFn: (state: T) => K): Signal { - return toSignal(this.select(selectorFn), { requireSync: true }); + selectSignal(selector: (state: T) => K): Signal { + return computed(() => selector(this.state())); } override lift(operator: Operator): Store { diff --git a/modules/store/src/to_signal.ts b/modules/store/src/to_signal.ts new file mode 100644 index 0000000000..070574da01 --- /dev/null +++ b/modules/store/src/to_signal.ts @@ -0,0 +1,28 @@ +import { signal, Signal } from '@angular/core'; +import { StateObservable } from './state'; + +/** + * Get the current value of an `StateObservable` as a reactive `Signal`. + * + * `toSignal` returns a `Signal` which provides synchronous reactive access to values produced + * by the `StateObservable`, by subscribing to that `StateObservable`. The returned `Signal` will always + * have the most recent value emitted by the subscription, and will throw an error if the + * `StateObservable` errors. + * + * The subscription will last for the lifetime of the application itself. + * + * This function is for internal use only as it differs from the `toSignal` + * provided by the `@angular/core/rxjs-interop` package with relying on + * the injection context to unsubscribe from the provided observable. + * + */ +export function toSignal(source: StateObservable): Signal { + let state = signal(undefined as T); + + source.subscribe({ + next: (value) => state.set(value), + error: (error) => state.set(error), + }); + + return state.asReadonly(); +} diff --git a/projects/example-app/src/app/auth/containers/login-page.component.ts b/projects/example-app/src/app/auth/containers/login-page.component.ts index 50cc47665f..04f1c8bbee 100644 --- a/projects/example-app/src/app/auth/containers/login-page.component.ts +++ b/projects/example-app/src/app/auth/containers/login-page.component.ts @@ -9,7 +9,7 @@ import { LoginPageActions } from '@example-app/auth/actions'; template: ` diff --git a/projects/example-app/src/app/core/containers/app.component.ts b/projects/example-app/src/app/core/containers/app.component.ts index afa122e638..fbef208647 100644 --- a/projects/example-app/src/app/core/containers/app.component.ts +++ b/projects/example-app/src/app/core/containers/app.component.ts @@ -11,7 +11,7 @@ import { LayoutActions } from '@example-app/core/actions'; changeDetection: ChangeDetectionStrategy.OnPush, template: ` - +