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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ $ npm install @ngxs/store@dev

### To become next version

- Fix(store): The deprecated `@Select` decorator no longer works on its own (it held the request's `Store` in a module-level static, leaking on the server). Apps still using `@Select` must add `withNgxsSelectDecoratorSupport()` to `provideStore(...)`, or `NgxsSelectDecoratorSupportModule.forRoot()` next to `NgxsModule.forRoot(...)` - or move to `store.select()` / `select()` / `store.selectSignal()` [#XXXX](https://github.com/ngxs/store/pull/XXXX)
- Feature(store): Warn when `setState`/`patchState` produce a new reference with an identical value [#2465](https://github.com/ngxs/store/pull/2465)
- Feature(store): Warn on duplicate action types via `warnOnDuplicateActionTypes` [#2466](https://github.com/ngxs/store/pull/2466)
- Feature(store): Add `ignoreUncompleted` action option [#2470](https://github.com/ngxs/store/pull/2470)
Expand Down
2 changes: 1 addition & 1 deletion docs/concepts/select/select-decorator.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
{% hint style="danger" %}
**DEPRECATED**

[Find out why we are deprecating the select decorator](../../deprecations/select-decorator-deprecation.md)
`@Select` is no longer enabled by default - it leaks on the server. Add `withNgxsSelectDecoratorSupport()` to `provideStore(...)` (or `NgxsSelectDecoratorSupportModule.forRoot()` next to `NgxsModule.forRoot(...)`) to keep using it, and see [why we are deprecating the select decorator](../../deprecations/select-decorator-deprecation.md).
{% endhint %}

You can select slices of data from the store using the `@Select` decorator. It has a few different ways to get your data out, whether passing the state class, a function, a different state class or a memoized selector.
Expand Down
23 changes: 22 additions & 1 deletion docs/deprecations/select-decorator-deprecation.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,28 @@

The `@Select` decorator is slated for removal in the future due to its inherent risks. It lacks integration with Angular's dependency injection system, making it prone to failures in scenarios with multiple simultaneous applications, such as server-side rendering and microfrontend setups.

Previously, the decorator stored the `Store` instance in a static variable, which could be overwritten by subsequent bootstrapped or removed applications. If a second application was created and destroyed before the first one, it could nullify the static variable, rendering the store inaccessible to the first application.
The decorator stores the `Store` instance in a static variable, which could be overwritten by subsequent bootstrapped or removed applications. If a second application was created and destroyed before the first one, it could nullify the static variable, rendering the store inaccessible to the first application. On the server, that same static keeps the request's `Store` (and its whole state graph) alive for the life of the process, so an app that doesn't use `@Select` still pays for it with a memory leak.

## `@Select` is no longer enabled by default

Because of the leak above, NGXS no longer creates the machinery `@Select` relies on unless you ask for it. If you can't migrate right away, opt in:

```ts
// standalone
provideStore([UsersState], withNgxsSelectDecoratorSupport());
```

```ts
// NgModule
@NgModule({
imports: [NgxsModule.forRoot([UsersState]), NgxsSelectDecoratorSupportModule.forRoot()]
})
export class AppModule {}
```

Without it, reading a `@Select` property throws. Migrating off `@Select` is still the recommended path.

## Migrating away from `@Select`

Every `@Select` usage should be replaced with the following:

Expand Down
55 changes: 55 additions & 0 deletions packages/store/src/decorators/select/select-decorator.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import {
EnvironmentProviders,
ModuleWithProviders,
NgModule,
inject,
provideEnvironmentInitializer
} from '@angular/core';

import { SelectFactory } from './select-factory';

/**
* Turns on the deprecated `@Select` decorator.
*
* `@Select` grabs the store from a static on `SelectFactory` - it has no
* injection context of its own. On the server that static hangs around between
* requests and pins the whole state tree, so NGXS doesn't create it for you
* anymore. Add this only if you still use `@Select`.
*
* ```ts
* provideStore([CountriesState], withNgxsSelectDecoratorSupport());
* ```
*
* @deprecated `@Select` is deprecated - move to `store.select()`, the functional
* `select()`, or `store.selectSignal()`:
* https://ngxs.io/deprecations/select-decorator-deprecation
*/
export function withNgxsSelectDecoratorSupport(): EnvironmentProviders {
return provideEnvironmentInitializer(() => inject(SelectFactory));
}

/**
* `NgModule` version of `withNgxsSelectDecoratorSupport()`, for apps still on
* `NgxsModule.forRoot()`.
*
* ```ts
* @NgModule({
* imports: [
* NgxsModule.forRoot([CountriesState]),
* NgxsSelectDecoratorSupportModule.forRoot()
* ]
* })
* export class AppModule {}
* ```
*
* @deprecated See `withNgxsSelectDecoratorSupport()`.
*/
@NgModule()
export class NgxsSelectDecoratorSupportModule {
static forRoot(): ModuleWithProviders<NgxsSelectDecoratorSupportModule> {
return {
ngModule: NgxsSelectDecoratorSupportModule,
providers: [withNgxsSelectDecoratorSupport()]
};
}
}
13 changes: 11 additions & 2 deletions packages/store/src/decorators/select/select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,17 @@ import { createSelectObservable, createSelectorFn, PropertyType } from './symbol
/**
* Decorator for selecting a slice of state from the store.
*
* @deprecated
* Read the deprecation notice at this link: https://ngxs.io/deprecations/select-decorator-deprecation.
* It doesn't work on its own anymore - opt in with
* `withNgxsSelectDecoratorSupport()` in `provideStore(...)`, or
* `NgxsSelectDecoratorSupportModule.forRoot()` next to `NgxsModule.forRoot(...)`:
*
* ```ts
* provideStore([CountriesState], withNgxsSelectDecoratorSupport());
* ```
*
* @deprecated Use `store.select()`, the functional `select()`, or
* `store.selectSignal()` instead:
* https://ngxs.io/deprecations/select-decorator-deprecation
*/
export function Select<T>(rawSelector?: T, ...paths: string[]): PropertyDecorator {
return function (target, key): void {
Expand Down
8 changes: 7 additions & 1 deletion packages/store/src/decorators/select/symbols.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,13 @@ export function propGetter(paths: string[], config: NgxsConfig) {
}

function throwSelectFactoryNotConnectedError(): never {
throw new Error('You have forgotten to import the NGXS module!');
throw new Error(
"`@Select` is deprecated and doesn't work on its own anymore. Add " +
'`withNgxsSelectDecoratorSupport()` to `provideStore(...)`, or ' +
'`NgxsSelectDecoratorSupportModule.forRoot()` next to `NgxsModule.forRoot(...)`. ' +
'Better to move to `store.select()`, `select()` or `store.selectSignal()`: ' +
'https://ngxs.io/deprecations/select-decorator-deprecation'
);
}

const DOLLAR_CHAR_CODE = 36;
Expand Down
4 changes: 4 additions & 0 deletions packages/store/src/public_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ export { Action } from './decorators/action';
export { Store } from './store';
export { State } from './decorators/state';
export { Select } from './decorators/select/select';
export {
withNgxsSelectDecoratorSupport,
NgxsSelectDecoratorSupportModule
} from './decorators/select/select-decorator.module';
export { SelectorOptions } from './decorators/selector-options';
export { Actions, type ActionContext, ActionStatus } from './actions-stream';

Expand Down
2 changes: 0 additions & 2 deletions packages/store/src/standalone-features/initializers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import { FEATURE_STATE_TOKEN, ROOT_STATE_TOKEN } from '../symbols';
import { StateFactory } from '../internal/state-factory';
import { StatesAndDefaults } from '../internal/internals';
import { assertRootStoreNotInitialized } from './root-guard';
import { SelectFactory } from '../decorators/select/select-factory';
import { InternalStateOperations } from '../internal/state-operations';
import { LifecycleStateManager } from '../internal/lifecycle-state-manager';
import { installOnUnhandhedErrorHandler } from '../internal/unhandled-rxjs-error-callback';
Expand Down Expand Up @@ -41,7 +40,6 @@ export function rootStoreInitializer(): void {
const internalStateOperations = inject(InternalStateOperations);

inject(Store);
inject(SelectFactory);

const states = inject(ROOT_STATE_TOKEN, { optional: true }) || [];
const lifecycleStateManager = inject(LifecycleStateManager);
Expand Down
8 changes: 6 additions & 2 deletions packages/store/tests/release-resources.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { BrowserModule } from '@angular/platform-browser';
import { NgModule, ErrorHandler, DoBootstrap } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { NgxsModule, Store } from '@ngxs/store';
import { NgxsModule, NgxsSelectDecoratorSupportModule, Store } from '@ngxs/store';
import { freshPlatform } from '@ngxs/store/internals/testing';

import { NoopErrorHandler } from './helpers/utils';
Expand All @@ -13,7 +13,11 @@ describe('Release NGXS resources', () => {
freshPlatform(async () => {
// Arrange
@NgModule({
imports: [BrowserModule, NgxsModule.forRoot([])],
imports: [
BrowserModule,
NgxsModule.forRoot([]),
NgxsSelectDecoratorSupportModule.forRoot()
],
providers: [{ provide: ErrorHandler, useClass: NoopErrorHandler }]
})
class TestModule implements DoBootstrap {
Expand Down
31 changes: 20 additions & 11 deletions packages/store/tests/select.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,16 @@ import { combineLatest, Observable, Subscription } from 'rxjs';
import { take } from 'rxjs/operators';
import { Component, Injectable, NgModule, inject } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { Store, NgxsModule, State, Action, Selector, Select, StateContext } from '@ngxs/store';
import {
Store,
NgxsModule,
NgxsSelectDecoratorSupportModule,
State,
Action,
Selector,
Select,
StateContext
} from '@ngxs/store';
import { skipConsoleLogging, freshPlatform } from '@ngxs/store/internals/testing';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

Expand Down Expand Up @@ -68,7 +77,7 @@ describe('Select', () => {

const states = [MySubState, MySubSubState, MyState];

it('should throw an exception when the user has forgotten to import the NGXS module', () => {
it('should throw an exception when `@Select` support has not been connected', () => {
// Arrange
let message: string | null = null;

Expand All @@ -84,7 +93,7 @@ describe('Select', () => {
}

// Assert
expect(message).toEqual('You have forgotten to import the NGXS module!');
expect(message).toContain('withNgxsSelectDecoratorSupport()');
});

it('should throw an exception when the component class is frozen', () => {
Expand All @@ -111,7 +120,7 @@ describe('Select', () => {
}

TestBed.configureTestingModule({
imports: [NgxsModule.forRoot(states)],
imports: [NgxsModule.forRoot(states), NgxsSelectDecoratorSupportModule.forRoot()],
declarations: [MySelectComponent]
});

Expand Down Expand Up @@ -143,7 +152,7 @@ describe('Select', () => {
}

TestBed.configureTestingModule({
imports: [NgxsModule.forRoot(states)],
imports: [NgxsModule.forRoot(states), NgxsSelectDecoratorSupportModule.forRoot()],
declarations: [SelectComponent]
});

Expand All @@ -170,7 +179,7 @@ describe('Select', () => {
}

TestBed.configureTestingModule({
imports: [NgxsModule.forRoot(states)],
imports: [NgxsModule.forRoot(states), NgxsSelectDecoratorSupportModule.forRoot()],
declarations: [StringSelectComponent]
});

Expand Down Expand Up @@ -204,7 +213,7 @@ describe('Select', () => {
}

TestBed.configureTestingModule({
imports: [NgxsModule.forRoot(states)],
imports: [NgxsModule.forRoot(states), NgxsSelectDecoratorSupportModule.forRoot()],
declarations: [StoreSelectComponent]
});

Expand Down Expand Up @@ -236,7 +245,7 @@ describe('Select', () => {
}

TestBed.configureTestingModule({
imports: [NgxsModule.forRoot(states)],
imports: [NgxsModule.forRoot(states), NgxsSelectDecoratorSupportModule.forRoot()],
declarations: [StoreSelectComponent]
});

Expand Down Expand Up @@ -267,7 +276,7 @@ describe('Select', () => {
}

TestBed.configureTestingModule({
imports: [NgxsModule.forRoot(states)],
imports: [NgxsModule.forRoot(states), NgxsSelectDecoratorSupportModule.forRoot()],
declarations: [StoreSelectComponent]
});

Expand Down Expand Up @@ -305,7 +314,7 @@ describe('Select', () => {
}

TestBed.configureTestingModule({
imports: [NgxsModule.forRoot(states)],
imports: [NgxsModule.forRoot(states), NgxsSelectDecoratorSupportModule.forRoot()],
declarations: [StoreSelectComponent]
});

Expand All @@ -332,7 +341,7 @@ describe('Select', () => {
}

TestBed.configureTestingModule({
imports: [NgxsModule.forRoot(states)],
imports: [NgxsModule.forRoot(states), NgxsSelectDecoratorSupportModule.forRoot()],
declarations: [StoreSelectComponent]
});

Expand Down
Loading