RxJS powered Redux state management for Angular applications
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
Feature management enables modular state organization with lazy-loaded features, automatic selector generation, and feature state isolation. It provides a structured approach to organizing large applications with multiple domains.
Creates feature objects with automatic selector generation and optional extra selectors for modular state management.
/**
* Creates a feature object with automatic selector generation
* @param config - Feature configuration with name and reducer
* @returns Feature object with selectors for state access
*/
function createFeature<FeatureName extends string, FeatureState>(
config: FeatureConfig<FeatureName, FeatureState>
): Feature<FeatureName, FeatureState>;
/**
* Creates a feature object with extra custom selectors
* @param config - Feature configuration with name, reducer, and extra selectors factory
* @returns Feature object with base and extra selectors
*/
function createFeature<FeatureName extends string, FeatureState, ExtraSelectors extends SelectorsDictionary>(
featureConfig: FeatureConfig<FeatureName, FeatureState> & {
extraSelectors: ExtraSelectorsFactory<FeatureName, FeatureState, ExtraSelectors>;
}
): FeatureWithExtraSelectors<FeatureName, FeatureState, ExtraSelectors>;
/**
* Basic feature configuration
*/
interface FeatureConfig<FeatureName extends string, FeatureState> {
/** Unique name/key for the feature */
name: FeatureName;
/** Reducer function for the feature state */
reducer: ActionReducer<FeatureState>;
}Usage Examples:
import { createFeature, createReducer, on } from "@ngrx/store";
import { loadUsers, loadUsersSuccess, loadUsersFailure } from "./user.actions";
// Define feature state
interface UserState {
entities: User[];
selectedUserId: string | null;
loading: boolean;
error: string | null;
filters: {
searchTerm: string;
role: string;
active: boolean;
};
}
const initialState: UserState = {
entities: [],
selectedUserId: null,
loading: false,
error: null,
filters: {
searchTerm: '',
role: 'all',
active: true
}
};
// Create feature reducer
const userReducer = createReducer(
initialState,
on(loadUsers, (state) => ({ ...state, loading: true, error: null })),
on(loadUsersSuccess, (state, { users }) => ({
...state,
entities: users,
loading: false
})),
on(loadUsersFailure, (state, { error }) => ({
...state,
loading: false,
error
}))
);
// Basic feature creation
export const userFeature = createFeature({
name: 'users',
reducer: userReducer
});
// Generated selectors automatically available:
// userFeature.selectUsersState - selects entire feature state
// userFeature.selectEntities - selects entities property
// userFeature.selectSelectedUserId - selects selectedUserId property
// userFeature.selectLoading - selects loading property
// userFeature.selectError - selects error property
// userFeature.selectFilters - selects filters property
// Usage in components
@Component({
template: `
<div>Users: {{ users() | json }}</div>
<div>Loading: {{ loading() }}</div>
`
})
export class UserListComponent {
private store = inject(Store);
users = this.store.selectSignal(userFeature.selectEntities);
loading = this.store.selectSignal(userFeature.selectLoading);
selectedUser = this.store.selectSignal(userFeature.selectSelectedUserId);
}Creates features with custom selectors in addition to automatically generated ones.
/**
* Factory function for creating extra selectors
*/
type ExtraSelectorsFactory<FeatureName extends string, FeatureState, ExtraSelectors> =
(baseSelectors: BaseSelectors<FeatureName, FeatureState>) => ExtraSelectors;
/**
* Dictionary of selector functions
*/
type SelectorsDictionary = Record<
string,
| Selector<Record<string, any>, unknown>
| ((...args: any[]) => Selector<Record<string, any>, unknown>)
>;Usage Examples:
import { createFeature, createSelector } from "@ngrx/store";
// Feature with extra selectors
export const userFeature = createFeature({
name: 'users',
reducer: userReducer,
extraSelectors: ({ selectUsersState, selectEntities, selectFilters, selectSelectedUserId }) => ({
// Derived selectors using base selectors
selectActiveUsers: createSelector(
selectEntities,
(entities) => entities.filter(user => user.active)
),
selectFilteredUsers: createSelector(
selectEntities,
selectFilters,
(entities, filters) => entities.filter(user => {
const matchesSearch = user.name.toLowerCase().includes(filters.searchTerm.toLowerCase());
const matchesRole = filters.role === 'all' || user.role === filters.role;
const matchesActive = user.active === filters.active;
return matchesSearch && matchesRole && matchesActive;
})
),
selectSelectedUser: createSelector(
selectEntities,
selectSelectedUserId,
(entities, selectedId) => selectedId ? entities.find(u => u.id === selectedId) : null
),
selectUserCount: createSelector(
selectEntities,
(entities) => entities.length
),
selectUsersByRole: createSelector(
selectEntities,
(entities) => entities.reduce((acc, user) => {
if (!acc[user.role]) acc[user.role] = [];
acc[user.role].push(user);
return acc;
}, {} as Record<string, User[]>)
),
// Selector factory for parameterized selection
selectUserById: (id: string) => createSelector(
selectEntities,
(entities) => entities.find(user => user.id === id)
)
})
});
// All selectors now available:
// Base: selectUsersState, selectEntities, selectLoading, etc.
// Extra: selectActiveUsers, selectFilteredUsers, selectSelectedUser, etc.
// Usage with extra selectors
@Component({
template: `
<div>Active Users: {{ activeUsers() | json }}</div>
<div>Filtered Users: {{ filteredUsers() | json }}</div>
<div>Selected: {{ selectedUser()?.name }}</div>
`
})
export class UserDashboardComponent {
private store = inject(Store);
activeUsers = this.store.selectSignal(userFeature.selectActiveUsers);
filteredUsers = this.store.selectSignal(userFeature.selectFilteredUsers);
selectedUser = this.store.selectSignal(userFeature.selectSelectedUser);
usersByRole = this.store.selectSignal(userFeature.selectUsersByRole);
}Features automatically generate selectors based on the state structure:
// For feature named 'users', generates:
selectUsersState: MemoizedSelector<Record<string, any>, UserState>// For each property in the state, generates selectors like:
selectEntities: MemoizedSelector<Record<string, any>, User[]>
selectLoading: MemoizedSelector<Record<string, any>, boolean>
selectError: MemoizedSelector<Record<string, any>, string | null>interface UserState {
filters: {
searchTerm: string;
role: string;
active: boolean;
};
}
// Generates flat selectors for nested properties:
selectFilters: MemoizedSelector<Record<string, any>, UserFilters>
selectSearchTerm: MemoizedSelector<Record<string, any>, string>
selectRole: MemoizedSelector<Record<string, any>, string>
selectActive: MemoizedSelector<Record<string, any>, boolean>Features work seamlessly with both module and standalone configurations:
import { StoreModule } from "@ngrx/store";
import { userFeature } from "./user.feature";
@NgModule({
imports: [
StoreModule.forFeature(userFeature)
]
})
export class UserModule {}import { provideState } from "@ngrx/store";
import { userFeature } from "./user.feature";
export const appConfig: ApplicationConfig = {
providers: [
provideState(userFeature),
// other providers
]
};interface ProductState {
products: {
entities: Product[];
selectedId: string | null;
loading: boolean;
};
categories: {
entities: Category[];
selectedId: string | null;
loading: boolean;
};
reviews: {
entities: Review[];
productReviews: Record<string, string[]>;
loading: boolean;
};
}
export const productFeature = createFeature({
name: 'products',
reducer: productReducer,
extraSelectors: ({
selectProducts,
selectCategories,
selectReviews
}) => ({
selectProductsWithCategories: createSelector(
selectProducts,
selectCategories,
(products, categories) => products.entities.map(product => ({
...product,
category: categories.entities.find(c => c.id === product.categoryId)
}))
),
selectProductReviews: (productId: string) => createSelector(
selectReviews,
(reviews) => {
const reviewIds = reviews.productReviews[productId] || [];
return reviewIds.map(id => reviews.entities.find(r => r.id === id)).filter(Boolean);
}
),
selectFeaturedProducts: createSelector(
selectProducts,
selectCategories,
(products, categories) => products.entities
.filter(p => p.featured)
.map(product => ({
...product,
category: categories.entities.find(c => c.id === product.categoryId)
}))
)
})
});// Combine multiple features for complex selectors
export const dashboardSelectors = {
selectDashboardData: createSelector(
userFeature.selectActiveUsers,
productFeature.selectFeaturedProducts,
orderFeature.selectRecentOrders,
(users, products, orders) => ({
activeUserCount: users.length,
featuredProductCount: products.length,
recentOrderCount: orders.length,
revenue: orders.reduce((sum, order) => sum + order.total, 0)
})
)
};Features include compile-time validation to ensure proper usage:
// ❌ Optional properties not allowed in feature state
interface BadFeatureState {
required: string;
optional?: string; // Error: optional properties not allowed
}
// ✅ All properties must be required
interface GoodFeatureState {
required: string;
alsoRequired: string;
}