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