Skip to content

Commit 91a52d1

Browse files
authored
feat(store): add withNgxsNoopExecutionStrategy (#2359)
1 parent a5f5cb0 commit 91a52d1

File tree

7 files changed

+147
-3
lines changed

7 files changed

+147
-3
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ $ npm install @ngxs/store@dev
66

77
### To become next patch version
88

9-
...
9+
- Feature(store): Add `withNgxsNoopExecutionStrategy` [#2359](https://github.com/ngxs/store/pull/2359)
1010

1111
### 20.0.2 2025-06-19
1212

packages/store/src/execution/execution-strategy.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { inject, Injectable, NgZone } from '@angular/core';
2+
import { NgxsExecutionStrategy } from './symbols';
23

34
@Injectable({ providedIn: 'root' })
4-
export class InternalNgxsExecutionStrategy {
5+
export class InternalNgxsExecutionStrategy implements NgxsExecutionStrategy {
56
private _ngZone = inject(NgZone);
67

78
enter<T>(func: () => T): T {
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { Injectable, makeEnvironmentProviders } from '@angular/core';
2+
import { InternalNgxsExecutionStrategy } from './execution-strategy';
3+
import type { NgxsExecutionStrategy } from './symbols';
4+
5+
@Injectable({ providedIn: 'root' })
6+
export class NoopNgxsExecutionStrategy implements NgxsExecutionStrategy {
7+
enter<T>(func: () => T): T {
8+
return func();
9+
}
10+
11+
leave<T>(func: () => T): T {
12+
return func();
13+
}
14+
}
15+
16+
export function withNgxsNoopExecutionStrategy() {
17+
return makeEnvironmentProviders([
18+
{
19+
provide: InternalNgxsExecutionStrategy,
20+
useExisting: NoopNgxsExecutionStrategy
21+
}
22+
]);
23+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/*
2+
* Execution strategy interface
3+
*/
4+
export interface NgxsExecutionStrategy {
5+
enter<T>(func: () => T): T;
6+
leave<T>(func: () => T): T;
7+
}

packages/store/src/public_api.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ export {
4242
} from './dev-features/ngxs-development.module';
4343
export { NgxsUnhandledActionsLogger } from './dev-features/ngxs-unhandled-actions-logger';
4444

45+
export { withNgxsNoopExecutionStrategy } from './execution/noop-execution-strategy';
46+
4547
export {
4648
createModelSelector,
4749
createPickSelector,

packages/store/tests/execution/default-execution-strategy.spec.ts renamed to packages/store/tests/execution/execution-strategy.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
} from '@ngxs/store';
1212
import { Observable } from 'rxjs';
1313

14-
describe('DispatchOutsideZoneNgxsExecutionStrategy', () => {
14+
describe('Default execution strategy', () => {
1515
class ZoneCounter {
1616
inside = 0;
1717
outside = 0;
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { Injectable, NgZone } from '@angular/core';
2+
import { TestBed } from '@angular/core/testing';
3+
import {
4+
Action,
5+
provideStore,
6+
Selector,
7+
State,
8+
StateContext,
9+
Store,
10+
withNgxsNoopExecutionStrategy
11+
} from '@ngxs/store';
12+
13+
describe('Noop execution strategy', () => {
14+
class ZoneCounter {
15+
inside = 0;
16+
outside = 0;
17+
hit() {
18+
if (NgZone.isInAngularZone()) {
19+
this.inside += 1;
20+
} else {
21+
this.outside += 1;
22+
}
23+
}
24+
25+
assert(expectation: { inside: number; outside: number }) {
26+
const self: ZoneCounter = this;
27+
expect({ ...self }).toEqual(expectation);
28+
}
29+
}
30+
31+
class Increment {
32+
static readonly type = '[Counter] Increment';
33+
}
34+
35+
@State<number>({
36+
name: 'counter',
37+
defaults: 0
38+
})
39+
@Injectable()
40+
class CounterState {
41+
@Selector()
42+
static getCounter(state: number) {
43+
return state;
44+
}
45+
46+
zoneCounter = new ZoneCounter();
47+
48+
@Action(Increment)
49+
increment({ setState, getState }: StateContext<number>): void {
50+
setState(getState() + 1);
51+
this.zoneCounter.hit();
52+
}
53+
}
54+
55+
function setup() {
56+
TestBed.configureTestingModule({
57+
providers: [provideStore([CounterState], withNgxsNoopExecutionStrategy())]
58+
});
59+
const store = TestBed.inject(Store);
60+
const zone: NgZone = TestBed.inject(NgZone);
61+
return { zone, store };
62+
}
63+
64+
describe('[store.select]', () => {
65+
it('should be performed outside Angular zone, when dispatched from outside zones', () => {
66+
// Arrange
67+
const { zone, store } = setup();
68+
const zoneCounter = new ZoneCounter();
69+
// Act
70+
zone.runOutsideAngular(() => {
71+
store
72+
.select<number>(({ counter }) => counter)
73+
.subscribe(() => {
74+
zoneCounter.hit();
75+
});
76+
77+
store.dispatch(new Increment());
78+
store.dispatch(new Increment());
79+
});
80+
81+
// Assert
82+
zoneCounter.assert({
83+
inside: 0,
84+
outside: 3
85+
});
86+
});
87+
88+
it('should be performed inside Angular zone, when dispatched from inside zones', () => {
89+
// Arrange
90+
const { zone, store } = setup();
91+
const zoneCounter = new ZoneCounter();
92+
// Act
93+
zone.run(() => {
94+
store
95+
.select<number>(({ counter }) => counter)
96+
.subscribe(() => {
97+
zoneCounter.hit();
98+
});
99+
100+
store.dispatch(new Increment());
101+
store.dispatch(new Increment());
102+
});
103+
104+
// Assert
105+
zoneCounter.assert({
106+
inside: 3,
107+
outside: 0
108+
});
109+
});
110+
});
111+
});

0 commit comments

Comments
 (0)