CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-ngrx--store

RxJS powered Redux state management for Angular applications

Pending
Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Pending

The risk profile of this skill

Overview
Eval results
Files

feature-management.mddocs/

Feature Management

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.

Capabilities

Create Feature

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);
}

Feature with Extra Selectors

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);
}

Automatic Selector Generation

Features automatically generate selectors based on the state structure:

Feature State Selector

// For feature named 'users', generates:
selectUsersState: MemoizedSelector<Record<string, any>, UserState>

Property Selectors

// 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>

Nested Object Properties

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>

Integration with Providers

Features work seamlessly with both module and standalone configurations:

Module Configuration

import { StoreModule } from "@ngrx/store";
import { userFeature } from "./user.feature";

@NgModule({
  imports: [
    StoreModule.forFeature(userFeature)
  ]
})
export class UserModule {}

Standalone Configuration

import { provideState } from "@ngrx/store";
import { userFeature } from "./user.feature";

export const appConfig: ApplicationConfig = {
  providers: [
    provideState(userFeature),
    // other providers
  ]
};

Advanced Feature Patterns

Multi-Entity Features

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)
        }))
    )
  })
});

Feature Composition

// 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)
    })
  )
};

Type Safety and Constraints

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;
}

Best Practices

  1. Feature Boundaries: Organize features around business domains, not technical concerns
  2. State Shape: Keep feature state flat and normalized when possible
  3. Selector Naming: Use descriptive names for extra selectors that indicate their purpose
  4. Composition: Combine features for cross-cutting concerns in separate selector files
  5. Testing: Test features and their selectors independently with mock state
  6. Lazy Loading: Use features with lazy-loaded modules for code splitting

docs

action-creators.md

feature-management.md

index.md

module-configuration.md

reducer-creators.md

selectors.md

standalone-providers.md

store-service.md

testing-utilities.md

tile.json