Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ $ npm install @ngxs/store@dev

### To become next patch version

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

### 20.0.2 2025-06-19

Expand Down
3 changes: 2 additions & 1 deletion packages/store/src/execution/execution-strategy.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { inject, Injectable, NgZone } from '@angular/core';
import { NgxsExecutionStrategy } from './symbols';

@Injectable({ providedIn: 'root' })
export class InternalNgxsExecutionStrategy {
export class InternalNgxsExecutionStrategy implements NgxsExecutionStrategy {
private _ngZone = inject(NgZone);

enter<T>(func: () => T): T {
Expand Down
23 changes: 23 additions & 0 deletions packages/store/src/execution/noop-execution-strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Injectable, makeEnvironmentProviders } from '@angular/core';
import { InternalNgxsExecutionStrategy } from './execution-strategy';
import type { NgxsExecutionStrategy } from './symbols';

@Injectable({ providedIn: 'root' })
export class NoopNgxsExecutionStrategy implements NgxsExecutionStrategy {
enter<T>(func: () => T): T {
return func();
}

leave<T>(func: () => T): T {
return func();
}
}

export function withNgxsNoopExecutionStrategy() {
return makeEnvironmentProviders([
{
provide: InternalNgxsExecutionStrategy,
useExisting: NoopNgxsExecutionStrategy
}
]);
}
7 changes: 7 additions & 0 deletions packages/store/src/execution/symbols.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/*
* Execution strategy interface
*/
export interface NgxsExecutionStrategy {
enter<T>(func: () => T): T;
leave<T>(func: () => T): T;
}
2 changes: 2 additions & 0 deletions packages/store/src/public_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ export {
} from './dev-features/ngxs-development.module';
export { NgxsUnhandledActionsLogger } from './dev-features/ngxs-unhandled-actions-logger';

export { withNgxsNoopExecutionStrategy } from './execution/noop-execution-strategy';

export {
createModelSelector,
createPickSelector,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
} from '@ngxs/store';
import { Observable } from 'rxjs';

describe('DispatchOutsideZoneNgxsExecutionStrategy', () => {
describe('Default execution strategy', () => {
class ZoneCounter {
inside = 0;
outside = 0;
Expand Down
111 changes: 111 additions & 0 deletions packages/store/tests/execution/noop-execution-strategy.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { Injectable, NgZone } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import {
Action,
provideStore,
Selector,
State,
StateContext,
Store,
withNgxsNoopExecutionStrategy
} from '@ngxs/store';

describe('Noop execution strategy', () => {
class ZoneCounter {
inside = 0;
outside = 0;
hit() {
if (NgZone.isInAngularZone()) {
this.inside += 1;
} else {
this.outside += 1;
}
}

assert(expectation: { inside: number; outside: number }) {
const self: ZoneCounter = this;
expect({ ...self }).toEqual(expectation);
}
}

class Increment {
static readonly type = '[Counter] Increment';
}

@State<number>({
name: 'counter',
defaults: 0
})
@Injectable()
class CounterState {
@Selector()
static getCounter(state: number) {
return state;
}

zoneCounter = new ZoneCounter();

@Action(Increment)
increment({ setState, getState }: StateContext<number>): void {
setState(getState() + 1);
this.zoneCounter.hit();
}
}

function setup() {
TestBed.configureTestingModule({
providers: [provideStore([CounterState], withNgxsNoopExecutionStrategy())]
});
const store = TestBed.inject(Store);
const zone: NgZone = TestBed.inject(NgZone);
return { zone, store };
}

describe('[store.select]', () => {
it('should be performed outside Angular zone, when dispatched from outside zones', () => {
// Arrange
const { zone, store } = setup();
const zoneCounter = new ZoneCounter();
// Act
zone.runOutsideAngular(() => {
store
.select<number>(({ counter }) => counter)
.subscribe(() => {
zoneCounter.hit();
});

store.dispatch(new Increment());
store.dispatch(new Increment());
});

// Assert
zoneCounter.assert({
inside: 0,
outside: 3
});
});

it('should be performed inside Angular zone, when dispatched from inside zones', () => {
// Arrange
const { zone, store } = setup();
const zoneCounter = new ZoneCounter();
// Act
zone.run(() => {
store
.select<number>(({ counter }) => counter)
.subscribe(() => {
zoneCounter.hit();
});

store.dispatch(new Increment());
store.dispatch(new Increment());
});

// Assert
zoneCounter.assert({
inside: 3,
outside: 0
});
});
});
});
Loading