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