Skip to content

Commit 5ac3e64

Browse files
fix(store): remove SelectorWithProps and MemoizedSelectorWithProps
Remove all deprecated selector-with-props APIs that were deprecated since v12 (issue #2980). This includes: - `SelectorWithProps` type from models - `MemoizedSelectorWithProps` interface from selector - 16 `createSelector` overloads accepting `SelectorWithProps` - 2 `createSelectorFactory` overloads returning `MemoizedSelectorWithProps` - `Store.select()` overloads accepting props and string keys - Standalone `select()` operator overloads accepting props and string keys - Related types in mock store/selector testing utilities - ESLint rule references to removed types - Documentation section on selectors with props Adds a v21 migration schematic that removes `SelectorWithProps` and `MemoizedSelectorWithProps` imports from user code, and updates the v21 migration guide with BEFORE/AFTER examples for converting to factory selectors. Closes #3035 BREAKING CHANGES: `SelectorWithProps` and `MemoizedSelectorWithProps` types have been removed. Use factory selectors instead. BEFORE: const selectCustomer = createSelector( selectCustomers, (customers, props: { customerId: number }) => customers[props.customerId] ); this.store.select(selectCustomer, { customerId: 42 }); AFTER: const selectCustomer = (customerId: number) => createSelector(selectCustomers, (customers) => customers[customerId]); this.store.select(selectCustomer(42)); String-key selectors have also been removed. BEFORE: this.store.select('featureName'); AFTER: this.store.select((state) => state.featureName); Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 151d7f2 commit 5ac3e64

22 files changed

Lines changed: 385 additions & 1184 deletions

File tree

modules/eslint-plugin/spec/rules/store/prefix-selectors-with-select.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ type Options = ESLintUtils.InferOptionsTypeFromRule<typeof rule>;
1515

1616
const valid: () => (string | ValidTestCase<Options>)[] = () => [
1717
`export const selectFeature: MemoizedSelector<any, any> = (state: AppState) => state.feature`,
18-
`export const selectFeature: MemoizedSelectorWithProps<any, any> = ({ feature }) => feature`,
18+
`export const selectFeature: Selector<any, any> = ({ feature }) => feature`,
1919
`export const selectFeature = createSelector((state: AppState) => state.feature)`,
2020
`export const selectFeature = createFeatureSelector<FeatureState>(featureKey)`,
2121
`export const selectFeature = createFeatureSelector<AppState, FeatureState>(featureKey)`,

modules/eslint-plugin/src/rules/store/prefix-selectors-with-select.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -159,12 +159,7 @@ export default createRule<Options, MessageIds>({
159159

160160
const hasSelectorType =
161161
typeName !== null &&
162-
[
163-
'MemoizedSelector',
164-
'MemoizedSelectorWithProps',
165-
'Selector',
166-
'SelectorWithProps',
167-
].includes(typeName);
162+
['MemoizedSelector', 'Selector'].includes(typeName);
168163

169164
const isSelectorCall =
170165
init?.type === 'CallExpression' && isSelectorFactoryCall(init);
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import {
2+
SchematicTestRunner,
3+
UnitTestTree,
4+
} from '@angular-devkit/schematics/testing';
5+
import { createWorkspace } from '@ngrx/schematics-core/testing';
6+
import * as path from 'path';
7+
import { tags } from '@angular-devkit/core';
8+
9+
describe('Store Migration to 21.0.0', () => {
10+
const collectionPath = path.join(
11+
process.cwd(),
12+
'dist/modules/store/migrations/migration.json'
13+
);
14+
const schematicRunner = new SchematicTestRunner('schematics', collectionPath);
15+
16+
let appTree: UnitTestTree;
17+
18+
beforeEach(async () => {
19+
appTree = await createWorkspace(schematicRunner, appTree);
20+
});
21+
22+
describe('removing SelectorWithProps and MemoizedSelectorWithProps', () => {
23+
it('should remove SelectorWithProps from import', async () => {
24+
const input = tags.stripIndent`
25+
import { createSelector, SelectorWithProps } from '@ngrx/store';
26+
27+
const selector = createSelector(
28+
(state: AppState) => state.items,
29+
(items) => items.length
30+
);
31+
`;
32+
const output = tags.stripIndent`
33+
import { createSelector } from '@ngrx/store';
34+
35+
const selector = createSelector(
36+
(state: AppState) => state.items,
37+
(items) => items.length
38+
);
39+
`;
40+
41+
appTree.create('main.ts', input);
42+
const tree = await schematicRunner.runSchematic(
43+
'ngrx-store-migration-21',
44+
{},
45+
appTree
46+
);
47+
const actual = tree.readContent('main.ts');
48+
expect(actual).toBe(output);
49+
});
50+
51+
it('should remove MemoizedSelectorWithProps from import', async () => {
52+
const input = tags.stripIndent`
53+
import { Store, MemoizedSelectorWithProps } from '@ngrx/store';
54+
55+
class MyComponent {
56+
constructor(private store: Store) {}
57+
}
58+
`;
59+
const output = tags.stripIndent`
60+
import { Store } from '@ngrx/store';
61+
62+
class MyComponent {
63+
constructor(private store: Store) {}
64+
}
65+
`;
66+
67+
appTree.create('main.ts', input);
68+
const tree = await schematicRunner.runSchematic(
69+
'ngrx-store-migration-21',
70+
{},
71+
appTree
72+
);
73+
const actual = tree.readContent('main.ts');
74+
expect(actual).toBe(output);
75+
});
76+
77+
it('should remove both SelectorWithProps and MemoizedSelectorWithProps from import', async () => {
78+
const input = tags.stripIndent`
79+
import { createSelector, SelectorWithProps, MemoizedSelectorWithProps } from '@ngrx/store';
80+
81+
const selector = createSelector(
82+
(state: AppState) => state.items,
83+
(items) => items.length
84+
);
85+
`;
86+
const output = tags.stripIndent`
87+
import { createSelector } from '@ngrx/store';
88+
89+
const selector = createSelector(
90+
(state: AppState) => state.items,
91+
(items) => items.length
92+
);
93+
`;
94+
95+
appTree.create('main.ts', input);
96+
const tree = await schematicRunner.runSchematic(
97+
'ngrx-store-migration-21',
98+
{},
99+
appTree
100+
);
101+
const actual = tree.readContent('main.ts');
102+
expect(actual).toBe(output);
103+
});
104+
105+
it('should remove entire import if only removed types are imported', async () => {
106+
const input = tags.stripIndent`
107+
import { SelectorWithProps } from '@ngrx/store';
108+
import { Component } from '@angular/core';
109+
`;
110+
const output = tags.stripIndent`
111+
import { Component } from '@angular/core';
112+
`;
113+
114+
appTree.create('main.ts', input);
115+
const tree = await schematicRunner.runSchematic(
116+
'ngrx-store-migration-21',
117+
{},
118+
appTree
119+
);
120+
const actual = tree.readContent('main.ts');
121+
expect(actual).toBe(output);
122+
});
123+
124+
it('should not modify files without removed types', async () => {
125+
const input = tags.stripIndent`
126+
import { createSelector, Store } from '@ngrx/store';
127+
128+
const selector = createSelector(
129+
(state: AppState) => state.items,
130+
(items) => items.length
131+
);
132+
`;
133+
134+
appTree.create('main.ts', input);
135+
const tree = await schematicRunner.runSchematic(
136+
'ngrx-store-migration-21',
137+
{},
138+
appTree
139+
);
140+
const actual = tree.readContent('main.ts');
141+
expect(actual).toBe(input);
142+
});
143+
144+
it('should handle double-quote imports', async () => {
145+
const input = tags.stripIndent`
146+
import { createSelector, SelectorWithProps } from "@ngrx/store";
147+
148+
const selector = createSelector(
149+
(state: AppState) => state.items,
150+
(items) => items.length
151+
);
152+
`;
153+
const output = tags.stripIndent`
154+
import { createSelector } from "@ngrx/store";
155+
156+
const selector = createSelector(
157+
(state: AppState) => state.items,
158+
(items) => items.length
159+
);
160+
`;
161+
162+
appTree.create('main.ts', input);
163+
const tree = await schematicRunner.runSchematic(
164+
'ngrx-store-migration-21',
165+
{},
166+
appTree
167+
);
168+
const actual = tree.readContent('main.ts');
169+
expect(actual).toBe(output);
170+
});
171+
});
172+
});
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import * as ts from 'typescript';
2+
import { Rule, SchematicContext, Tree } from '@angular-devkit/schematics';
3+
import {
4+
Change,
5+
commitChanges,
6+
createReplaceChange,
7+
visitTSSourceFiles,
8+
} from '../../schematics-core';
9+
import { createRemoveChange } from '../../schematics-core/utility/change';
10+
11+
const removedTypes = ['SelectorWithProps', 'MemoizedSelectorWithProps'];
12+
13+
export default function migrateRemoveSelectorWithProps(): Rule {
14+
return (tree: Tree, ctx: SchematicContext) => {
15+
visitTSSourceFiles(tree, (sourceFile) => {
16+
const changes: Change[] = [];
17+
18+
const importDeclarations = sourceFile.statements.filter(
19+
ts.isImportDeclaration
20+
);
21+
22+
for (const importDecl of importDeclarations) {
23+
const moduleSpecifier = importDecl.moduleSpecifier;
24+
if (
25+
!ts.isStringLiteral(moduleSpecifier) ||
26+
moduleSpecifier.text !== '@ngrx/store'
27+
) {
28+
continue;
29+
}
30+
31+
const namedBindings = importDecl.importClause?.namedBindings;
32+
if (!namedBindings || !ts.isNamedImports(namedBindings)) {
33+
continue;
34+
}
35+
36+
const removedElements = namedBindings.elements.filter((el) =>
37+
removedTypes.includes(
38+
(el.propertyName ?? el.name).getText(sourceFile)
39+
)
40+
);
41+
42+
if (removedElements.length === 0) {
43+
continue;
44+
}
45+
46+
const remainingElements = namedBindings.elements.filter(
47+
(el) =>
48+
!removedTypes.includes(
49+
(el.propertyName ?? el.name).getText(sourceFile)
50+
)
51+
);
52+
53+
if (remainingElements.length === 0) {
54+
// All imports are removed types — remove the entire import declaration
55+
changes.push(
56+
createRemoveChange(
57+
sourceFile,
58+
importDecl,
59+
importDecl.getStart(sourceFile),
60+
importDecl.getEnd() + 1
61+
)
62+
);
63+
} else {
64+
// Rebuild the import without the removed types
65+
const remainingImports = remainingElements
66+
.map((el) => el.getText(sourceFile))
67+
.join(', ');
68+
const quote = importDecl.moduleSpecifier
69+
.getText(sourceFile)
70+
.charAt(0);
71+
const newImport = `import { ${remainingImports} } from ${quote}@ngrx/store${quote};`;
72+
changes.push(
73+
createReplaceChange(
74+
sourceFile,
75+
importDecl,
76+
importDecl.getText(sourceFile),
77+
newImport
78+
)
79+
);
80+
}
81+
82+
const removedNames = removedElements.map((el) =>
83+
(el.propertyName ?? el.name).getText(sourceFile)
84+
);
85+
ctx.logger.info(
86+
`[@ngrx/store] ${sourceFile.fileName}: Removed ${removedNames.join(', ')} import(s). ` +
87+
`Use factory selectors instead. See https://ngrx.io/guide/migration/v21`
88+
);
89+
}
90+
91+
if (changes.length) {
92+
commitChanges(tree, sourceFile.fileName, changes);
93+
}
94+
});
95+
};
96+
}

modules/store/migrations/migration.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@
4040
"description": "As of NgRx v18, the `TypedAction` has been removed in favor of `Action`.",
4141
"version": "18-beta",
4242
"factory": "./18_0_0-beta/index"
43+
},
44+
"ngrx-store-migration-21": {
45+
"description": "As of NgRx v21, `SelectorWithProps` and `MemoizedSelectorWithProps` have been removed. Use factory selectors instead.",
46+
"version": "21",
47+
"factory": "./21_0_0/index"
4348
}
4449
}
4550
}

modules/store/spec/edge.spec.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,16 @@ describe('ngRx Store', () => {
3434
let todosNextCount = 0;
3535
let todosCountNextCount = 0;
3636

37-
store.pipe(select('todos')).subscribe((todos) => {
37+
store.pipe(select((state: any) => state.todos)).subscribe((todos) => {
3838
todosNextCount++;
3939
store.dispatch({ type: 'SET_COUNT', payload: todos.length });
4040
});
4141

42-
store.pipe(select('todoCount')).subscribe((count) => {
43-
todosCountNextCount++;
44-
});
42+
store
43+
.pipe(select((state: any) => state.todoCount))
44+
.subscribe((count) => {
45+
todosCountNextCount++;
46+
});
4547

4648
store.dispatch({ type: 'ADD_TODO', payload: { name: 'test' } });
4749
expect(todosNextCount).toBe(2);

modules/store/spec/feature_creator.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ describe('createFeature()', () => {
225225
});
226226

227227
TestBed.inject(Store)
228-
.select(fooFeature.name)
228+
.select((state: any) => state[fooFeature.name])
229229
.pipe(take(1))
230230
.subscribe((fooState) => {
231231
expect(fooState).toEqual(initialFooState);

0 commit comments

Comments
 (0)