@malamute/ai-rules 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +174 -0
- package/bin/cli.js +5 -0
- package/configs/_shared/.claude/commands/fix-issue.md +38 -0
- package/configs/_shared/.claude/commands/generate-tests.md +49 -0
- package/configs/_shared/.claude/commands/review-pr.md +77 -0
- package/configs/_shared/.claude/rules/accessibility.md +270 -0
- package/configs/_shared/.claude/rules/performance.md +226 -0
- package/configs/_shared/.claude/rules/security.md +188 -0
- package/configs/_shared/.claude/skills/debug/SKILL.md +118 -0
- package/configs/_shared/.claude/skills/learning/SKILL.md +224 -0
- package/configs/_shared/.claude/skills/review/SKILL.md +86 -0
- package/configs/_shared/.claude/skills/spec/SKILL.md +112 -0
- package/configs/_shared/CLAUDE.md +174 -0
- package/configs/angular/.claude/rules/components.md +257 -0
- package/configs/angular/.claude/rules/state.md +250 -0
- package/configs/angular/.claude/rules/testing.md +422 -0
- package/configs/angular/.claude/settings.json +31 -0
- package/configs/angular/CLAUDE.md +251 -0
- package/configs/dotnet/.claude/rules/api.md +370 -0
- package/configs/dotnet/.claude/rules/architecture.md +199 -0
- package/configs/dotnet/.claude/rules/database/efcore.md +408 -0
- package/configs/dotnet/.claude/rules/testing.md +389 -0
- package/configs/dotnet/.claude/settings.json +9 -0
- package/configs/dotnet/CLAUDE.md +319 -0
- package/configs/nestjs/.claude/rules/auth.md +321 -0
- package/configs/nestjs/.claude/rules/database/prisma.md +305 -0
- package/configs/nestjs/.claude/rules/database/typeorm.md +379 -0
- package/configs/nestjs/.claude/rules/modules.md +215 -0
- package/configs/nestjs/.claude/rules/testing.md +315 -0
- package/configs/nestjs/.claude/rules/validation.md +279 -0
- package/configs/nestjs/.claude/settings.json +15 -0
- package/configs/nestjs/CLAUDE.md +263 -0
- package/configs/nextjs/.claude/rules/components.md +211 -0
- package/configs/nextjs/.claude/rules/state/redux-toolkit.md +429 -0
- package/configs/nextjs/.claude/rules/state/zustand.md +299 -0
- package/configs/nextjs/.claude/rules/testing.md +315 -0
- package/configs/nextjs/.claude/settings.json +29 -0
- package/configs/nextjs/CLAUDE.md +376 -0
- package/configs/python/.claude/rules/database/sqlalchemy.md +355 -0
- package/configs/python/.claude/rules/fastapi.md +272 -0
- package/configs/python/.claude/rules/flask.md +332 -0
- package/configs/python/.claude/rules/testing.md +374 -0
- package/configs/python/.claude/settings.json +18 -0
- package/configs/python/CLAUDE.md +273 -0
- package/package.json +41 -0
- package/src/install.js +315 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "libs/**/feature/**/*.ts"
|
|
4
|
+
- "libs/**/ui/**/*.ts"
|
|
5
|
+
- "apps/**/*.component.ts"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Component Rules (Angular 21)
|
|
9
|
+
|
|
10
|
+
## Code Quality
|
|
11
|
+
|
|
12
|
+
### No Useless Comments
|
|
13
|
+
|
|
14
|
+
Code must be self-documenting. Variable and function names must be explicit.
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
// BAD
|
|
18
|
+
const c = getConfig(); // get the config
|
|
19
|
+
items.filter(x => x.active);
|
|
20
|
+
|
|
21
|
+
// GOOD
|
|
22
|
+
const applicationConfig = getConfig();
|
|
23
|
+
items.filter(item => item.active);
|
|
24
|
+
users.map(user => user.email);
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Never Disable Lint Rules Without Justification
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
// FORBIDDEN - no explanation
|
|
31
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
32
|
+
|
|
33
|
+
// ALLOWED - only with explicit justification
|
|
34
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Third-party API returns untyped response, ticket ABC-123 to add types
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### ChangeDetectionStrategy.OnPush
|
|
38
|
+
|
|
39
|
+
Use `OnPush` for performance optimization. Required for:
|
|
40
|
+
- All UI components (dumb components)
|
|
41
|
+
- Feature components with signal-based state
|
|
42
|
+
- Any component where you control the inputs
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
@Component({
|
|
46
|
+
changeDetection: ChangeDetectionStrategy.OnPush, // Always add this
|
|
47
|
+
})
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## File Structure
|
|
51
|
+
|
|
52
|
+
Always use separate files for template and styles:
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
user-list/
|
|
56
|
+
user-list.component.ts
|
|
57
|
+
user-list.component.html
|
|
58
|
+
user-list.component.scss
|
|
59
|
+
user-list.component.spec.ts
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## @Component Decorator
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
@Component({
|
|
66
|
+
selector: 'app-user-list',
|
|
67
|
+
imports: [SomeComponent, SomePipe], // Direct imports
|
|
68
|
+
templateUrl: './user-list.component.html', // Always external
|
|
69
|
+
styleUrl: './user-list.component.scss', // Always external
|
|
70
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
71
|
+
})
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**Do NOT include:**
|
|
75
|
+
- `standalone: true` (it's the default)
|
|
76
|
+
- `template:` inline templates
|
|
77
|
+
- `styles:` inline styles
|
|
78
|
+
|
|
79
|
+
## Smart Components (feature/)
|
|
80
|
+
|
|
81
|
+
Smart components are located in `feature/` libraries.
|
|
82
|
+
|
|
83
|
+
### Allowed
|
|
84
|
+
|
|
85
|
+
- Inject `Store` from NgRx
|
|
86
|
+
- Dispatch actions
|
|
87
|
+
- Use `selectSignal()` for state
|
|
88
|
+
- Handle routing and navigation
|
|
89
|
+
- Contain page-level logic
|
|
90
|
+
|
|
91
|
+
### Required
|
|
92
|
+
|
|
93
|
+
- Pass data to UI components via `input()` signals
|
|
94
|
+
- Handle events from UI components via `output()`
|
|
95
|
+
- Use `ChangeDetectionStrategy.OnPush`
|
|
96
|
+
- Use `templateUrl` (external template file)
|
|
97
|
+
|
|
98
|
+
### Example
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
// product-page.component.ts
|
|
102
|
+
@Component({
|
|
103
|
+
selector: 'app-product-page',
|
|
104
|
+
imports: [ProductListComponent, ProductFiltersComponent],
|
|
105
|
+
templateUrl: './product-page.component.html',
|
|
106
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
107
|
+
})
|
|
108
|
+
export class ProductPageComponent {
|
|
109
|
+
private readonly store = inject(Store);
|
|
110
|
+
|
|
111
|
+
categories = this.store.selectSignal(selectCategories);
|
|
112
|
+
filteredProducts = this.store.selectSignal(selectFilteredProducts);
|
|
113
|
+
loading = this.store.selectSignal(selectProductsLoading);
|
|
114
|
+
|
|
115
|
+
onFilterChange(filters: Filters): void {
|
|
116
|
+
this.store.dispatch(ProductActions.setFilters({ filters }));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
onAddToCart(product: Product): void {
|
|
120
|
+
this.store.dispatch(CartActions.addItem({ product }));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
```html
|
|
126
|
+
<!-- product-page.component.html -->
|
|
127
|
+
<app-product-filters
|
|
128
|
+
[categories]="categories()"
|
|
129
|
+
(filterChange)="onFilterChange($event)"
|
|
130
|
+
/>
|
|
131
|
+
|
|
132
|
+
<app-product-list
|
|
133
|
+
[products]="filteredProducts()"
|
|
134
|
+
[loading]="loading()"
|
|
135
|
+
(addToCart)="onAddToCart($event)"
|
|
136
|
+
/>
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## UI Components (ui/)
|
|
140
|
+
|
|
141
|
+
UI components are located in `ui/` libraries. They are purely presentational.
|
|
142
|
+
|
|
143
|
+
### Forbidden
|
|
144
|
+
|
|
145
|
+
- NO `Store` injection
|
|
146
|
+
- NO service injection (except pure utility services)
|
|
147
|
+
- NO direct HTTP calls
|
|
148
|
+
- NO router navigation
|
|
149
|
+
- NO business logic
|
|
150
|
+
|
|
151
|
+
### Required
|
|
152
|
+
|
|
153
|
+
- Use `input()` and `input.required()` for data
|
|
154
|
+
- Use `output()` for events
|
|
155
|
+
- Use `model()` for two-way binding
|
|
156
|
+
- Use `ChangeDetectionStrategy.OnPush`
|
|
157
|
+
- Must be fully testable in isolation
|
|
158
|
+
|
|
159
|
+
### Example
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
// product-card.component.ts
|
|
163
|
+
@Component({
|
|
164
|
+
selector: 'app-product-card',
|
|
165
|
+
imports: [CurrencyPipe],
|
|
166
|
+
templateUrl: './product-card.component.html',
|
|
167
|
+
styleUrl: './product-card.component.scss',
|
|
168
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
169
|
+
})
|
|
170
|
+
export class ProductCardComponent {
|
|
171
|
+
product = input.required<Product>();
|
|
172
|
+
addToCart = output<Product>();
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
```html
|
|
177
|
+
<!-- product-card.component.html -->
|
|
178
|
+
<article class="product-card">
|
|
179
|
+
<img [src]="product().image" [alt]="product().name" />
|
|
180
|
+
<h3>{{ product().name }}</h3>
|
|
181
|
+
<p class="price">{{ product().price | currency }}</p>
|
|
182
|
+
<button type="button" (click)="addToCart.emit(product())">
|
|
183
|
+
Add to Cart
|
|
184
|
+
</button>
|
|
185
|
+
</article>
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Signal Inputs/Outputs/Model
|
|
189
|
+
|
|
190
|
+
Always use signal-based functions, NOT decorators:
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
// Inputs - use input(), NOT @Input()
|
|
194
|
+
name = input<string>(); // Optional
|
|
195
|
+
name = input('default'); // With default
|
|
196
|
+
name = input.required<string>(); // Required
|
|
197
|
+
|
|
198
|
+
// Outputs - use output(), NOT @Output()
|
|
199
|
+
clicked = output<void>();
|
|
200
|
+
selected = output<Item>();
|
|
201
|
+
|
|
202
|
+
// Two-way binding - use model()
|
|
203
|
+
value = model<string>(''); // Optional with default
|
|
204
|
+
value = model.required<string>(); // Required
|
|
205
|
+
|
|
206
|
+
// Emit
|
|
207
|
+
this.clicked.emit();
|
|
208
|
+
this.selected.emit(item);
|
|
209
|
+
this.value.set('new value'); // model is writable
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Two-way Binding with model()
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
// Component
|
|
216
|
+
@Component({
|
|
217
|
+
selector: 'app-search-input',
|
|
218
|
+
templateUrl: './search-input.component.html',
|
|
219
|
+
})
|
|
220
|
+
export class SearchInputComponent {
|
|
221
|
+
query = model(''); // Creates [(query)] binding
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
```html
|
|
226
|
+
<!-- Parent usage -->
|
|
227
|
+
<app-search-input [(query)]="searchQuery" />
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## Control Flow (in templates)
|
|
231
|
+
|
|
232
|
+
Use the built-in control flow syntax:
|
|
233
|
+
|
|
234
|
+
```html
|
|
235
|
+
<!-- Conditionals -->
|
|
236
|
+
@if (loading()) {
|
|
237
|
+
<app-spinner />
|
|
238
|
+
} @else if (error()) {
|
|
239
|
+
<app-error [message]="error()" />
|
|
240
|
+
} @else {
|
|
241
|
+
<app-content [data]="data()" />
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
<!-- Loops - always use track! -->
|
|
245
|
+
@for (item of items(); track item.id) {
|
|
246
|
+
<app-item [item]="item" />
|
|
247
|
+
} @empty {
|
|
248
|
+
<p>No items found</p>
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
<!-- Switch -->
|
|
252
|
+
@switch (status()) {
|
|
253
|
+
@case ('loading') { <app-spinner /> }
|
|
254
|
+
@case ('error') { <app-error /> }
|
|
255
|
+
@default { <app-content /> }
|
|
256
|
+
}
|
|
257
|
+
```
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "libs/**/data-access/**/*.ts"
|
|
4
|
+
- "libs/**/+state/**/*.ts"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# NgRx State Management Rules
|
|
8
|
+
|
|
9
|
+
## File Structure
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
libs/[domain]/data-access/
|
|
13
|
+
src/lib/
|
|
14
|
+
+state/
|
|
15
|
+
[domain].actions.ts
|
|
16
|
+
[domain].reducer.ts
|
|
17
|
+
[domain].effects.ts
|
|
18
|
+
[domain].selectors.ts
|
|
19
|
+
[domain].adapter.ts # If using @ngrx/entity
|
|
20
|
+
[domain].state.ts # State interface
|
|
21
|
+
services/
|
|
22
|
+
[domain].service.ts # API calls
|
|
23
|
+
index.ts # Public API
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Actions
|
|
27
|
+
|
|
28
|
+
Use `createActionGroup` for related actions:
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
import { createActionGroup, emptyProps, props } from '@ngrx/store';
|
|
32
|
+
|
|
33
|
+
export const UserActions = createActionGroup({
|
|
34
|
+
source: 'Users',
|
|
35
|
+
events: {
|
|
36
|
+
// Load
|
|
37
|
+
'Load Users': emptyProps(),
|
|
38
|
+
'Load Users Success': props<{ users: User[] }>(),
|
|
39
|
+
'Load Users Failure': props<{ error: string }>(),
|
|
40
|
+
|
|
41
|
+
// CRUD
|
|
42
|
+
'Add User': props<{ user: User }>(),
|
|
43
|
+
'Update User': props<{ update: Update<User> }>(),
|
|
44
|
+
'Delete User': props<{ id: string }>(),
|
|
45
|
+
|
|
46
|
+
// Selection
|
|
47
|
+
'Select User': props<{ id: string }>(),
|
|
48
|
+
'Clear Selection': emptyProps(),
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Reducer with Entity Adapter
|
|
54
|
+
|
|
55
|
+
Always use `@ngrx/entity` for collections:
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
// [domain].adapter.ts
|
|
59
|
+
import { createEntityAdapter, EntityAdapter, EntityState } from '@ngrx/entity';
|
|
60
|
+
|
|
61
|
+
export interface UserState extends EntityState<User> {
|
|
62
|
+
selectedId: string | null;
|
|
63
|
+
loading: boolean;
|
|
64
|
+
error: string | null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const userAdapter: EntityAdapter<User> = createEntityAdapter<User>({
|
|
68
|
+
selectId: (user) => user.id,
|
|
69
|
+
sortComparer: (a, b) => a.name.localeCompare(b.name),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
export const initialUserState: UserState = userAdapter.getInitialState({
|
|
73
|
+
selectedId: null,
|
|
74
|
+
loading: false,
|
|
75
|
+
error: null,
|
|
76
|
+
});
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
// [domain].reducer.ts
|
|
81
|
+
import { createReducer, on } from '@ngrx/store';
|
|
82
|
+
|
|
83
|
+
export const userReducer = createReducer(
|
|
84
|
+
initialUserState,
|
|
85
|
+
|
|
86
|
+
on(UserActions.loadUsers, (state) => ({
|
|
87
|
+
...state,
|
|
88
|
+
loading: true,
|
|
89
|
+
error: null,
|
|
90
|
+
})),
|
|
91
|
+
|
|
92
|
+
on(UserActions.loadUsersSuccess, (state, { users }) =>
|
|
93
|
+
userAdapter.setAll(users, { ...state, loading: false })
|
|
94
|
+
),
|
|
95
|
+
|
|
96
|
+
on(UserActions.loadUsersFailure, (state, { error }) => ({
|
|
97
|
+
...state,
|
|
98
|
+
loading: false,
|
|
99
|
+
error,
|
|
100
|
+
})),
|
|
101
|
+
|
|
102
|
+
on(UserActions.addUser, (state, { user }) =>
|
|
103
|
+
userAdapter.addOne(user, state)
|
|
104
|
+
),
|
|
105
|
+
|
|
106
|
+
on(UserActions.updateUser, (state, { update }) =>
|
|
107
|
+
userAdapter.updateOne(update, state)
|
|
108
|
+
),
|
|
109
|
+
|
|
110
|
+
on(UserActions.deleteUser, (state, { id }) =>
|
|
111
|
+
userAdapter.removeOne(id, state)
|
|
112
|
+
),
|
|
113
|
+
|
|
114
|
+
on(UserActions.selectUser, (state, { id }) => ({
|
|
115
|
+
...state,
|
|
116
|
+
selectedId: id,
|
|
117
|
+
})),
|
|
118
|
+
|
|
119
|
+
on(UserActions.clearSelection, (state) => ({
|
|
120
|
+
...state,
|
|
121
|
+
selectedId: null,
|
|
122
|
+
}))
|
|
123
|
+
);
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Selectors
|
|
127
|
+
|
|
128
|
+
Use adapter selectors and compose them:
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
// [domain].selectors.ts
|
|
132
|
+
import { createFeatureSelector, createSelector } from '@ngrx/store';
|
|
133
|
+
|
|
134
|
+
// Feature selector
|
|
135
|
+
export const selectUserState = createFeatureSelector<UserState>('users');
|
|
136
|
+
|
|
137
|
+
// Adapter selectors
|
|
138
|
+
const { selectAll, selectEntities, selectIds, selectTotal } =
|
|
139
|
+
userAdapter.getSelectors();
|
|
140
|
+
|
|
141
|
+
// Exported selectors
|
|
142
|
+
export const selectAllUsers = createSelector(selectUserState, selectAll);
|
|
143
|
+
|
|
144
|
+
export const selectUserEntities = createSelector(selectUserState, selectEntities);
|
|
145
|
+
|
|
146
|
+
export const selectUsersLoading = createSelector(
|
|
147
|
+
selectUserState,
|
|
148
|
+
(state) => state.loading
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
export const selectUsersError = createSelector(
|
|
152
|
+
selectUserState,
|
|
153
|
+
(state) => state.error
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
export const selectSelectedUserId = createSelector(
|
|
157
|
+
selectUserState,
|
|
158
|
+
(state) => state.selectedId
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
export const selectSelectedUser = createSelector(
|
|
162
|
+
selectUserEntities,
|
|
163
|
+
selectSelectedUserId,
|
|
164
|
+
(entities, selectedId) => (selectedId ? entities[selectedId] : null)
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
// Derived/computed selectors
|
|
168
|
+
export const selectActiveUsers = createSelector(
|
|
169
|
+
selectAllUsers,
|
|
170
|
+
(users) => users.filter((u) => u.active)
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
export const selectUserCount = createSelector(
|
|
174
|
+
selectUserState,
|
|
175
|
+
selectTotal
|
|
176
|
+
);
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Effects
|
|
180
|
+
|
|
181
|
+
Keep effects clean and focused:
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
// [domain].effects.ts
|
|
185
|
+
import { inject } from '@angular/core';
|
|
186
|
+
import { Actions, createEffect, ofType } from '@ngrx/effects';
|
|
187
|
+
import { catchError, exhaustMap, map, of } from 'rxjs';
|
|
188
|
+
|
|
189
|
+
export const loadUsers$ = createEffect(
|
|
190
|
+
(actions$ = inject(Actions), userService = inject(UserService)) =>
|
|
191
|
+
actions$.pipe(
|
|
192
|
+
ofType(UserActions.loadUsers),
|
|
193
|
+
exhaustMap(() =>
|
|
194
|
+
userService.getAll().pipe(
|
|
195
|
+
map((users) => UserActions.loadUsersSuccess({ users })),
|
|
196
|
+
catchError((error) =>
|
|
197
|
+
of(UserActions.loadUsersFailure({ error: error.message }))
|
|
198
|
+
)
|
|
199
|
+
)
|
|
200
|
+
)
|
|
201
|
+
),
|
|
202
|
+
{ functional: true }
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
export const addUser$ = createEffect(
|
|
206
|
+
(actions$ = inject(Actions), userService = inject(UserService)) =>
|
|
207
|
+
actions$.pipe(
|
|
208
|
+
ofType(UserActions.addUser),
|
|
209
|
+
exhaustMap(({ user }) =>
|
|
210
|
+
userService.create(user).pipe(
|
|
211
|
+
map((created) => UserActions.addUserSuccess({ user: created })),
|
|
212
|
+
catchError((error) =>
|
|
213
|
+
of(UserActions.addUserFailure({ error: error.message }))
|
|
214
|
+
)
|
|
215
|
+
)
|
|
216
|
+
)
|
|
217
|
+
),
|
|
218
|
+
{ functional: true }
|
|
219
|
+
);
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## RxJS Operator Guidelines
|
|
223
|
+
|
|
224
|
+
| Scenario | Operator |
|
|
225
|
+
|----------|----------|
|
|
226
|
+
| Single request, cancel previous | `switchMap` |
|
|
227
|
+
| Single request, ignore while pending | `exhaustMap` |
|
|
228
|
+
| Queue all requests | `concatMap` |
|
|
229
|
+
| Parallel requests | `mergeMap` |
|
|
230
|
+
|
|
231
|
+
## Store Usage in Components
|
|
232
|
+
|
|
233
|
+
Only in smart components (feature/):
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
// Use selectSignal for signal-based selection
|
|
237
|
+
users = this.store.selectSignal(selectAllUsers);
|
|
238
|
+
loading = this.store.selectSignal(selectUsersLoading);
|
|
239
|
+
|
|
240
|
+
// Dispatch actions
|
|
241
|
+
this.store.dispatch(UserActions.loadUsers());
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## Anti-patterns to Avoid
|
|
245
|
+
|
|
246
|
+
- Never store derived state in the store (use selectors)
|
|
247
|
+
- Never dispatch actions from effects that trigger the same effect
|
|
248
|
+
- Never use `store.select()` in UI components
|
|
249
|
+
- Never mutate state directly
|
|
250
|
+
- Avoid fat effects - keep business logic in services
|