Skip to content

Commit 8cb5795

Browse files
fix(store): move Angular Signal interop into State service (#3879)
Closes #3869
1 parent 0aa1582 commit 8cb5795

File tree

8 files changed

+90
-9
lines changed

8 files changed

+90
-9
lines changed

modules/data/spec/selectors/entity-selectors$.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Action, MemoizedSelector, Store } from '@ngrx/store';
1+
import { Action, MemoizedSelector, StateObservable, Store } from '@ngrx/store';
22

33
import { BehaviorSubject, Observable, Subject } from 'rxjs';
44
import {
@@ -75,7 +75,7 @@ describe('EntitySelectors$', () => {
7575
actions$ = new Subject<Action>();
7676
state$ = new BehaviorSubject({ entityCache: emptyCache });
7777
store = new Store<{ entityCache: EntityCache }>(
78-
state$,
78+
state$ as unknown as StateObservable,
7979
null as any,
8080
null as any
8181
);
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { TestBed } from '@angular/core/testing';
2+
import { Store, provideStore } from '@ngrx/store';
3+
import { Component, inject, Pipe, PipeTransform } from '@angular/core';
4+
5+
@Pipe({ name: 'test', standalone: true })
6+
export class TestPipe implements PipeTransform {
7+
store = inject(Store);
8+
9+
transform(s: number) {
10+
this.store.select('count');
11+
return s * 2;
12+
}
13+
}
14+
15+
@Component({
16+
selector: 'test-component',
17+
standalone: true,
18+
imports: [TestPipe],
19+
template: `{{ 3 | test }}`,
20+
})
21+
export class TestComponent {}
22+
23+
describe('NgRx Store Integration', () => {
24+
describe('with pipes', () => {
25+
beforeEach(() => {
26+
TestBed.configureTestingModule({
27+
imports: [TestComponent],
28+
providers: [
29+
provideStore({ count: () => 2 }, { initialState: { count: 2 } }),
30+
],
31+
});
32+
});
33+
34+
it('should not throw an error', () => {
35+
const component = TestBed.createComponent(TestComponent);
36+
37+
component.detectChanges();
38+
39+
expect(component.nativeElement.textContent).toBe('6');
40+
});
41+
});
42+
});

modules/store/src/state.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { Inject, Injectable, OnDestroy, Provider } from '@angular/core';
1+
import { Inject, Injectable, OnDestroy, Provider, Signal } from '@angular/core';
2+
import { toSignal } from '@angular/core/rxjs-interop';
23
import {
34
BehaviorSubject,
45
Observable,
@@ -13,14 +14,24 @@ import { ReducerObservable } from './reducer_manager';
1314
import { ScannedActionsSubject } from './scanned_actions_subject';
1415
import { INITIAL_STATE } from './tokens';
1516

16-
export abstract class StateObservable extends Observable<any> {}
17+
export abstract class StateObservable extends Observable<any> {
18+
/**
19+
* @internal
20+
*/
21+
abstract readonly state: Signal<any>;
22+
}
1723

1824
@Injectable()
1925
export class State<T> extends BehaviorSubject<any> implements OnDestroy {
2026
static readonly INIT = INIT;
2127

2228
private stateSubscription: Subscription;
2329

30+
/**
31+
* @internal
32+
*/
33+
public state: Signal<T>;
34+
2435
constructor(
2536
actions$: ActionsSubject,
2637
reducer$: ReducerObservable,
@@ -50,6 +61,8 @@ export class State<T> extends BehaviorSubject<any> implements OnDestroy {
5061
this.next(state);
5162
scannedActions.next(action as Action);
5263
});
64+
65+
this.state = toSignal(this, { manualCleanup: true, requireSync: true });
5366
}
5467

5568
ngOnDestroy() {

modules/store/src/store.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import { computed, Injectable, Provider, Signal } from '@angular/core';
33
import { Observable, Observer, Operator } from 'rxjs';
44
import { distinctUntilChanged, map, pluck } from 'rxjs/operators';
5-
import { toSignal } from '@angular/core/rxjs-interop';
65

76
import { ActionsSubject } from './actions_subject';
87
import {
@@ -19,7 +18,10 @@ export class Store<T = object>
1918
extends Observable<T>
2019
implements Observer<Action>
2120
{
22-
private readonly state: Signal<T>;
21+
/**
22+
* @internal
23+
*/
24+
readonly state: Signal<T>;
2325

2426
constructor(
2527
state$: StateObservable,
@@ -29,7 +31,7 @@ export class Store<T = object>
2931
super();
3032

3133
this.source = state$;
32-
this.state = toSignal(state$, { manualCleanup: true });
34+
this.state = state$.state;
3335
}
3436

3537
select<K>(mapFn: (state: T) => K): Observable<K>;
Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
1-
import { Injectable } from '@angular/core';
1+
import { Injectable, Signal } from '@angular/core';
2+
import { toSignal } from '@angular/core/rxjs-interop';
23
import { BehaviorSubject } from 'rxjs';
34

45
@Injectable()
56
export class MockState<T> extends BehaviorSubject<T> {
7+
/**
8+
* @internal
9+
*/
10+
readonly state: Signal<T>;
11+
612
constructor() {
713
super(<T>{});
14+
15+
this.state = toSignal(this, { manualCleanup: true, requireSync: true });
816
}
917
}

projects/standalone-app/src/app/app.component.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import { TestBed } from '@angular/core/testing';
22
import { AppComponent } from './app.component';
33
import { RouterTestingModule } from '@angular/router/testing';
4+
import { provideMockStore } from '@ngrx/store/testing';
45

56
describe('AppComponent', () => {
67
beforeEach(async () => {
78
await TestBed.configureTestingModule({
89
imports: [RouterTestingModule, AppComponent],
10+
providers: [provideMockStore()],
911
}).compileComponents();
1012
});
1113

projects/standalone-app/src/app/app.component.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,19 @@ import {
55
INITIAL_STATE_TOKEN,
66
provideComponentStore,
77
} from '@ngrx/component-store';
8+
import { TestPipe } from './test.pipe';
89

910
@Component({
1011
selector: 'ngrx-root',
1112
standalone: true,
12-
imports: [RouterModule],
13+
imports: [RouterModule, TestPipe],
1314
template: `
1415
<h1>Welcome {{ title }} {{ val() }}</h1>
1516
1617
<a routerLink="/feature">Load Feature</a>
1718
19+
{{ 3 | test }}
20+
1821
<router-outlet></router-outlet>
1922
`,
2023
providers: [
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { inject, Pipe, PipeTransform } from '@angular/core';
2+
import { Store } from '@ngrx/store';
3+
4+
@Pipe({ name: 'test', standalone: true })
5+
export class TestPipe implements PipeTransform {
6+
store = inject(Store);
7+
transform(s: number) {
8+
this.store.select('count');
9+
return s * 2;
10+
}
11+
}

0 commit comments

Comments
 (0)