state-implementation

State Implementation Skill

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "state-implementation" with this command: npx skills add pluginagentmarketplace/custom-plugin-angular/pluginagentmarketplace-custom-plugin-angular-state-implementation

State Implementation Skill

Quick Start

Simple Service-Based State

import { Injectable } from '@angular/core'; import { BehaviorSubject, Observable } from 'rxjs';

@Injectable({ providedIn: 'root' }) export class UserStore { private usersSubject = new BehaviorSubject<User[]>([]); users$ = this.usersSubject.asObservable();

constructor(private http: HttpClient) {}

loadUsers() { this.http.get<User[]>('/api/users').subscribe( users => this.usersSubject.next(users) ); }

addUser(user: User) { this.http.post<User>('/api/users', user).subscribe( newUser => { const current = this.usersSubject.value; this.usersSubject.next([...current, newUser]); } ); } }

// Usage export class UserListComponent { users$ = this.userStore.users$;

constructor(private userStore: UserStore) {} }

NgRx Basics

// 1. Define actions export const loadUsers = createAction('[User] Load Users'); export const loadUsersSuccess = createAction( '[User] Load Users Success', props<{ users: User[] }>() ); export const loadUsersError = createAction( '[User] Load Users Error', props<{ error: string }>() );

// 2. Create reducer const initialState: UserState = { users: [], loading: false };

export const userReducer = createReducer( initialState, on(loadUsers, state => ({ ...state, loading: true })), on(loadUsersSuccess, (state, { users }) => ({ ...state, users, loading: false })), on(loadUsersError, (state, { error }) => ({ ...state, error, loading: false })) );

// 3. Create effect @Injectable() export class UserEffects { loadUsers$ = createEffect(() => this.actions$.pipe( ofType(loadUsers), switchMap(() => this.userService.getUsers().pipe( map(users => loadUsersSuccess({ users })), catchError(error => of(loadUsersError({ error }))) ) ) ) );

constructor( private actions$: Actions, private userService: UserService ) {} }

// 4. Use in component @Component({...}) export class UserListComponent { users$ = this.store.select(selectUsers); loading$ = this.store.select(selectLoading);

constructor(private store: Store) { this.store.dispatch(loadUsers()); } }

NgRx Core Concepts

Store

// Dispatch action this.store.dispatch(loadUsers());

// Select state this.store.select(selectUsers).subscribe(users => { console.log(users); });

// Select with observable this.users$ = this.store.select(selectUsers);

// Multiple selects this.store.select(selectUsers, selectLoading).subscribe(([users, loading]) => { // ... });

Selectors

// Feature selector export const selectUserState = createFeatureSelector<UserState>('users');

// Select from feature export const selectUsers = createSelector( selectUserState, state => state.users );

// Selector composition export const selectActiveUsers = createSelector( selectUsers, users => users.filter(u => u.active) );

// Memoized selector export const selectUserById = (id: number) => createSelector( selectUsers, users => users.find(u => u.id === id) );

// With props export const selectUsersByRole = createSelector( selectUsers, (users: User[], { role }: { role: string }) => users.filter(u => u.role === role) );

// Usage with props this.store.select(selectUsersByRole, { role: 'admin' });

Effects

// Side effect - HTTP call @Injectable() export class UserEffects { loadUsers$ = createEffect(() => this.actions$.pipe( ofType(UserActions.loadUsers), switchMap(() => this.userService.getUsers().pipe( map(users => UserActions.loadUsersSuccess({ users })), catchError(error => of(UserActions.loadUsersError({ error }))) ) ) ) );

// Non-dispatching effect logActions$ = createEffect( () => this.actions$.pipe( tap(action => console.log(action)) ), { dispatch: false } );

constructor( private actions$: Actions, private userService: UserService ) {} }

Entity Adapter

Setup

export interface User { id: number; name: string; email: string; }

export const adapter = createEntityAdapter<User>({ selectId: (user: User) => user.id, sortComparer: (a: User, b: User) => a.name.localeCompare(b.name) });

export interface UserState extends EntityState<User> { loading: boolean; error: string | null; }

const initialState = adapter.getInitialState({ loading: false, error: null });

Reducer with Adapter

export const userReducer = createReducer( initialState, on(loadUsers, state => ({ ...state, loading: true })), on(loadUsersSuccess, (state, { users }) => adapter.setAll(users, { ...state, loading: false }) ), on(addUserSuccess, (state, { user }) => adapter.addOne(user, state) ), on(updateUserSuccess, (state, { user }) => adapter.updateOne({ id: user.id, changes: user }, state) ), on(deleteUserSuccess, (state, { id }) => adapter.removeOne(id, state) ) );

// Export selectors export const { selectIds, selectEntities, selectAll, selectTotal } = adapter.getSelectors(selectUserState);

Facade Pattern

@Injectable() export class UserFacade { users$ = this.store.select(selectAllUsers); loading$ = this.store.select(selectUsersLoading); error$ = this.store.select(selectUsersError);

constructor(private store: Store) {}

loadUsers() { this.store.dispatch(loadUsers()); }

addUser(user: User) { this.store.dispatch(addUser({ user })); }

updateUser(id: number, changes: Partial<User>) { this.store.dispatch(updateUser({ id, changes })); }

deleteUser(id: number) { this.store.dispatch(deleteUser({ id })); } }

// Component usage simplified @Component({...}) export class UserListComponent { users$ = this.userFacade.users$; loading$ = this.userFacade.loading$;

constructor(private userFacade: UserFacade) { this.userFacade.loadUsers(); } }

Angular Signals

import { signal, computed, effect } from '@angular/core';

// Create signal const count = signal(0);

// Read value console.log(count()); // 0

// Update value count.set(1); count.update(c => c + 1);

// Computed value const doubled = computed(() => count() * 2);

// Effect effect(() => { console.log(Count is ${count()}); console.log(Doubled is ${doubled()}); });

// Signal-based state @Component({...}) export class CounterComponent { count = signal(0); doubled = computed(() => this.count() * 2);

increment() { this.count.update(c => c + 1); } }

HTTP Integration

HttpClient with Interceptor

@Injectable() export class AuthInterceptor implements HttpInterceptor { constructor(private authService: AuthService) {}

intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { const token = this.authService.getToken(); const authReq = req.clone({ setHeaders: { Authorization: Bearer ${token} } }); return next.handle(authReq); } }

// Register @NgModule({ providers: [ { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true } ] }) export class AppModule { }

Caching Strategy

@Injectable() export class CachingService { private cache = new Map<string, any>();

get<T>(key: string, request: Observable<T>, ttl: number = 3600000): Observable<T> { if (this.cache.has(key)) { return of(this.cache.get(key)); }

return request.pipe(
  tap(data => {
    this.cache.set(key, data);
    setTimeout(() => this.cache.delete(key), ttl);
  })
);

} }

// Usage getUsers() { return this.caching.get( 'users', this.http.get<User[]>('/api/users'), 5 * 60 * 1000 // 5 minutes ); }

Testing State

describe('User Store', () => { let store: MockStore;

beforeEach(() => { TestBed.configureTestingModule({ imports: [StoreModule.forRoot({ users: userReducer })] }); store = TestBed.inject(Store) as MockStore; });

it('should load users', () => { const action = loadUsers(); const completion = loadUsersSuccess({ users: mockUsers });

const effect$ = new UserEffects(
  hot('a', { a: action }),
  mockUserService
).loadUsers$;

const result = cold('b', { b: completion });
expect(effect$).toBeObservable(result);

});

it('should select users', (done) => { store.setState({ users: { users: mockUsers } }); store.select(selectUsers).subscribe(users => { expect(users).toEqual(mockUsers); done(); }); }); });

Best Practices

  • Normalize State: Flat structure, avoid nesting

  • Single Responsibility: Each reducer handles one feature

  • Use Facades: Simplify component-store interaction

  • Memoize Selectors: Prevent unnecessary recalculations

  • Handle Errors: Always include error states

  • Lazy Load Stores: Register feature stores when needed

  • Time-Travel Debugging: Use Redux DevTools

Advanced Patterns

Composition Pattern

// Combine multiple stores @Injectable() export class AppFacade { users$ = this.userFacade.users$; products$ = this.productFacade.products$; cart$ = this.cartFacade.cart$;

constructor( private userFacade: UserFacade, private productFacade: ProductFacade, private cartFacade: CartFacade ) {} }

Feature Flags

export const selectFeatureFlags = createFeatureSelector<FeatureFlags>('features'); export const selectFeatureEnabled = (feature: string) => createSelector( selectFeatureFlags, flags => flags[feature]?.enabled ?? false );

// Component <div *ngIf="featureEnabled$ | async">New Feature</div>

Resources

  • NgRx Documentation

  • Entity Adapter

  • DevTools

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Automation

angular-material

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

rxjs-implementation

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

routing-performance-implementation

No summary provided by upstream source.

Repository SourceNeeds Review