STATE MANAGEMENT
Ecosistema completo para organización y gestión de contenido con interfaces espaciales
State Management Patterns - User Management Module
Comprehensive guide for working with NGXS state in the user-management module.
Table of Contents
- Architecture Overview
- State Structure
- Actions Reference
- Selectors Usage
- Common Patterns
- Event Integration
- Best Practices
- Performance Optimization
Architecture Overview
The user-management module uses NGXS for state management with the following architecture:
┌─────────────────────────────────────────────────────────┐
│ Components/Services │
│ (Dispatch Actions) │
└─────────────────────┬───────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ NGXS Store │
│ (UserState) │
└─────────────────────┬───────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Action Handlers │
│ (Call Services, Emit Events) │
└─────────────────────┬───────────────────────────────────┘
│
┌────────────┴────────────┐
▼ ▼
┌──────────────────┐ ┌───────────────────┐
│ UserApiService │ │ EventBusService │
│ (HTTP Calls) │ │ (Domain Events) │
└──────────────────┘ └───────────────────┘
Key Principles:
- Unidirectional data flow
- Immutable state updates
- Single source of truth
- Predictable state changes
- Event-driven side effects
State Structure
UserStateModel Interface
export interface UserStateModel {
/** List of users */
users: User[];
/** Currently selected user (for detail view) */
selectedUser: User | null;
/** Loading state for async operations */
loading: boolean;
/** Error message from failed operations */
error: string | null;
/** Current filter criteria */
filters: UserFilters;
/** IDs of users selected for bulk operations */
bulkSelection: string[];
/** Total count of users (for pagination) */
total: number;
}
Default State
{
users: [],
selectedUser: null,
loading: false,
error: null,
filters: {},
bulkSelection: [],
total: 0
}
Actions Reference
Load Operations
LoadUsers
Fetches users from API with optional filters.
import { LoadUsers } from '@libs-core/user-management';
// Load all users
this.store.dispatch(new LoadUsers());
// Load with filters
this.store.dispatch(new LoadUsers({
search: 'john',
status: ['active'],
roleIds: ['admin-role-id']
}));
State Changes:
- Sets
loading = truebefore request - On success: updates
usersandtotal, setsloading = false, clearserror - On failure: sets
error, setsloading = false
LoadUsersSuccess / LoadUsersFailure
Internal actions - dispatched automatically by LoadUsers handler.
CRUD Operations
CreateUser
Creates a new user.
import { CreateUser } from '@libs-core/user-management';
this.store.dispatch(new CreateUser({
email: '[email protected]',
username: 'johndoe',
firstName: 'John',
lastName: 'Doe',
primaryRole: SystemRoles.USER
}));
State Changes:
- Adds new user to
usersarray - Emits
UserCreatedEvent
Events Published:
{
eventName: 'user.created',
payload: {
user: User,
createdBy: string,
tenantId: string,
correlationId?: string
}
}
UpdateUser
Updates existing user.
import { UpdateUser } from '@libs-core/user-management';
this.store.dispatch(new UpdateUser({
userId: 'user-123',
firstName: 'Jane',
lastName: 'Smith'
}));
State Changes:
- Updates user in
usersarray - Updates
selectedUserif it matches - Emits
UserUpdatedEventwith previous data
DeleteUser
Deletes a user.
import { DeleteUser } from '@libs-core/user-management';
this.store.dispatch(new DeleteUser('user-123'));
State Changes:
- Removes user from
usersarray - Clears
selectedUserif it matches - Emits
UserDeletedEvent(high priority)
Selection Operations
SelectUser
Sets the currently selected user.
import { SelectUser } from '@libs-core/user-management';
this.store.dispatch(new SelectUser(user));
// Clear selection
this.store.dispatch(new SelectUser(null));
State Changes:
- Sets
selectedUser
Use Case: Detail views, edit forms
ClearSelection
Clears the selected user.
import { ClearSelection } from '@libs-core/user-management';
this.store.dispatch(new ClearSelection());
Bulk Operations
SelectMultipleUsers
Selects users for bulk operations.
import { SelectMultipleUsers } from '@libs-core/user-management';
this.store.dispatch(new SelectMultipleUsers([
'user-1',
'user-2',
'user-3'
]));
State Changes:
- Sets
bulkSelectionarray
ClearBulkSelection
Clears bulk selection.
import { ClearBulkSelection } from '@libs-core/user-management';
this.store.dispatch(new ClearBulkSelection());
BulkUpdateUsers
Performs bulk operation on selected users.
import { BulkUpdateUsers } from '@libs-core/user-management';
this.store.dispatch(new BulkUpdateUsers({
operation: 'activate', // 'activate' | 'deactivate' | 'delete' | 'changeRole'
userIds: ['user-1', 'user-2'],
options: {
// Operation-specific options
newRole: 'manager-role-id' // For changeRole operation
}
}));
State Changes:
- Updates affected users in
usersarray - Clears
bulkSelection - Emits
UserBulkOperationEvent
Events Published:
{
eventName: 'user.bulk.operation',
payload: {
operation: 'activate' | 'deactivate' | 'delete' | 'changeRole',
userIds: string[],
successCount: number,
failureCount: number,
executedBy: string,
tenantId: string
}
}
Filter Operations
SetFilters
Updates filter criteria.
import { SetFilters } from '@libs-core/user-management';
this.store.dispatch(new SetFilters({
search: 'john',
status: ['active', 'pending'],
roleIds: ['admin-role', 'manager-role'],
createdAfter: '2024-01-01',
createdBefore: '2024-12-31'
}));
State Changes:
- Updates
filters - Triggers automatic re-fetch of users with new filters
ClearFilters
Resets all filters.
import { ClearFilters } from '@libs-core/user-management';
this.store.dispatch(new ClearFilters());
State Changes:
- Resets
filtersto{} - Triggers re-fetch with no filters
Status Operations
ChangeUserStatus
Changes user status with optional reason.
import { ChangeUserStatus } from '@libs-core/user-management';
this.store.dispatch(new ChangeUserStatus({
userId: 'user-123',
newStatus: 'suspended',
reason: 'Policy violation'
}));
State Changes:
- Updates user status in
usersarray - Emits
UserStatusChangedEvent
Events Published:
{
eventName: 'user.status.changed',
payload: {
user: User,
previousStatus: string,
newStatus: string,
changedBy: string,
tenantId: string,
reason?: string
}
}
Selectors Usage
Basic Selectors
import { UserState } from '@libs-core/user-management';
import { Store } from '@ngxs/store';
export class MyComponent {
private store = inject(Store);
// Get all users
users$ = this.store.select(UserState.getUsers);
// Get selected user
selectedUser$ = this.store.select(UserState.getSelectedUser);
// Get loading state
loading$ = this.store.select(UserState.isLoading);
// Get error
error$ = this.store.select(UserState.getError);
// Get current filters
filters$ = this.store.select(UserState.getFilters);
// Get bulk selection
bulkSelection$ = this.store.select(UserState.getBulkSelection);
// Get total count
total$ = this.store.select(UserState.getTotal);
}
Using with Signals
import { toSignal } from '@angular/core/rxjs-interop';
export class MyComponent {
users = toSignal(this.store.select(UserState.getUsers), {
initialValue: []
});
selectedUser = toSignal(this.store.select(UserState.getSelectedUser));
loading = toSignal(this.store.select(UserState.isLoading), {
initialValue: false
});
}
Computed Values
export class MyComponent {
users = toSignal(this.store.select(UserState.getUsers), { initialValue: [] });
// Computed: active users only
activeUsers = computed(() => {
return this.users().filter(u => u.status === 'active');
});
// Computed: admin users
adminUsers = computed(() => {
return this.users().filter(u =>
u.roles.some(r => r.name === SystemRoles.ADMIN)
);
});
// Computed: user count
userCount = computed(() => this.users().length);
// Computed: has selected user
hasSelection = computed(() => this.selectedUser() !== null);
}
Common Patterns
Pattern 1: Load-Display-Error
Complete pattern for loading data with error handling:
export class UserListPageComponent {
private store = inject(Store);
users = toSignal(this.store.select(UserState.getUsers), { initialValue: [] });
loading = toSignal(this.store.select(UserState.isLoading), { initialValue: false });
error = toSignal(this.store.select(UserState.getError));
ngOnInit() {
// Load users on component init
this.store.dispatch(new LoadUsers());
// Handle errors
effect(() => {
const errorMessage = this.error();
if (errorMessage) {
this.showErrorNotification(errorMessage);
}
});
}
private showErrorNotification(message: string) {
// Show error toast/notification
}
}
Pattern 2: Filter-Search-Pagination
Complete pattern for filtered, searchable, paginated lists:
export class UserListComponent {
private store = inject(Store);
users = toSignal(this.store.select(UserState.getUsers), { initialValue: [] });
total = toSignal(this.store.select(UserState.getTotal), { initialValue: 0 });
filters = toSignal(this.store.select(UserState.getFilters), { initialValue: {} });
// Pagination state
currentPage = signal(1);
pageSize = signal(20);
// Computed: paginated users
paginatedUsers = computed(() => {
const start = (this.currentPage() - 1) * this.pageSize();
const end = start + this.pageSize();
return this.users().slice(start, end);
});
// Apply filters
applyFilters(filters: UserFilters) {
this.store.dispatch(new SetFilters(filters));
this.currentPage.set(1); // Reset to first page
}
// Search
search(query: string) {
this.store.dispatch(new SetFilters({ ...this.filters(), search: query }));
this.currentPage.set(1);
}
// Pagination
goToPage(page: number) {
this.currentPage.set(page);
}
}
Pattern 3: Create-Edit Form
Complete pattern for form submission with state management:
export class UserFormComponent {
private store = inject(Store);
private router = inject(Router);
mode = signal<'create' | 'edit'>('create');
userId = input<string | null>(null);
selectedUser = toSignal(this.store.select(UserState.getSelectedUser));
loading = toSignal(this.store.select(UserState.isLoading), { initialValue: false });
ngOnInit() {
const id = this.userId();
if (id) {
this.mode.set('edit');
// Load user for editing
this.loadUser(id);
}
}
private loadUser(id: string) {
// Load and select user
this.store.dispatch(new LoadUsers()).subscribe(() => {
const users = this.store.selectSnapshot(UserState.getUsers);
const user = users.find(u => u.id === id);
if (user) {
this.store.dispatch(new SelectUser(user));
}
});
}
onSubmit(formData: CreateUserRequest | UpdateUserRequest) {
if (this.mode() === 'create') {
this.store.dispatch(new CreateUser(formData as CreateUserRequest))
.subscribe({
next: () => this.router.navigate(['/users']),
error: (error) => this.handleError(error)
});
} else {
this.store.dispatch(new UpdateUser({
userId: this.userId()!,
...formData
}))
.subscribe({
next: () => this.router.navigate(['/users']),
error: (error) => this.handleError(error)
});
}
}
onCancel() {
this.router.navigate(['/users']);
}
private handleError(error: unknown) {
console.error('Form submission failed:', error);
}
}
Pattern 4: Bulk Operations
Complete pattern for bulk user operations:
export class UserBulkActionsComponent {
private store = inject(Store);
bulkSelection = toSignal(this.store.select(UserState.getBulkSelection), {
initialValue: []
});
hasSelection = computed(() => this.bulkSelection().length > 0);
selectionCount = computed(() => this.bulkSelection().length);
// Select users
selectUsers(userIds: string[]) {
this.store.dispatch(new SelectMultipleUsers(userIds));
}
// Clear selection
clearSelection() {
this.store.dispatch(new ClearBulkSelection());
}
// Bulk activate
activateSelected() {
if (!this.hasSelection()) return;
this.store.dispatch(new BulkUpdateUsers({
operation: 'activate',
userIds: this.bulkSelection(),
options: {}
}));
}
// Bulk deactivate
deactivateSelected() {
if (!this.hasSelection()) return;
this.store.dispatch(new BulkUpdateUsers({
operation: 'deactivate',
userIds: this.bulkSelection(),
options: {}
}));
}
// Bulk delete (with confirmation)
deleteSelected() {
if (!this.hasSelection()) return;
const confirmed = confirm(
`Delete ${this.selectionCount()} users? This cannot be undone.`
);
if (confirmed) {
this.store.dispatch(new BulkUpdateUsers({
operation: 'delete',
userIds: this.bulkSelection(),
options: {}
}));
}
}
// Bulk change role
changeRoleForSelected(newRoleId: string) {
if (!this.hasSelection()) return;
this.store.dispatch(new BulkUpdateUsers({
operation: 'changeRole',
userIds: this.bulkSelection(),
options: { newRole: newRoleId }
}));
}
}
Event Integration
Subscribing to State-Triggered Events
The UserState automatically publishes domain events. Subscribe in other modules:
import { EventBusService } from '@libs-core/event-system';
import {
UserCreatedEvent,
UserUpdatedEvent,
UserDeletedEvent,
UserStatusChangedEvent,
UserBulkOperationEvent
} from '@libs-core/user-management';
export class AuditLogService {
private eventBus = inject(EventBusService);
constructor() {
this.subscribeToUserEvents();
}
private subscribeToUserEvents() {
// Log user creation
this.eventBus.subscribe(UserCreatedEvent, (event) => {
this.logAudit({
action: 'USER_CREATED',
userId: event.payload.user.id,
performedBy: event.payload.createdBy,
tenantId: event.payload.tenantId,
timestamp: event.metadata.timestamp,
data: { email: event.payload.user.email }
});
});
// Log user updates
this.eventBus.subscribe(UserUpdatedEvent, (event) => {
this.logAudit({
action: 'USER_UPDATED',
userId: event.payload.user.id,
performedBy: event.payload.updatedBy,
tenantId: event.payload.tenantId,
timestamp: event.metadata.timestamp,
data: {
previousData: event.payload.previousData,
newData: event.payload.user
}
});
});
// Log user deletion (high priority)
this.eventBus.subscribe(UserDeletedEvent, (event) => {
this.logAudit({
action: 'USER_DELETED',
userId: event.payload.userId,
performedBy: event.payload.deletedBy,
tenantId: event.payload.tenantId,
timestamp: event.metadata.timestamp,
priority: 'HIGH',
data: { email: event.payload.userEmail }
});
});
}
private logAudit(entry: AuditLogEntry) {
// Send to audit log system
}
}
Best Practices
1. Use Action Creators
Always use the action classes, never construct manually:
// ✅ Correct
this.store.dispatch(new LoadUsers(filters));
// ❌ Wrong
this.store.dispatch({ type: '[User] Load Users', filters });
2. Handle Async Operations
Always handle both success and error cases:
this.store.dispatch(new CreateUser(request))
.subscribe({
next: () => {
this.showSuccess('User created successfully');
this.router.navigate(['/users']);
},
error: (error) => {
this.showError('Failed to create user');
console.error(error);
}
});
3. Avoid Nested Subscriptions
Use operators like switchMap instead:
// ❌ Bad: nested subscriptions
this.store.dispatch(new LoadUsers()).subscribe(() => {
this.store.dispatch(new SelectUser(user)).subscribe(() => {
// More actions...
});
});
// ✅ Good: use RxJS operators
import { Actions, ofActionSuccessful } from '@ngxs/store';
export class MyComponent {
private actions$ = inject(Actions);
ngOnInit() {
this.actions$
.pipe(
ofActionSuccessful(LoadUsersSuccess),
switchMap(() => this.store.dispatch(new SelectUser(user)))
)
.subscribe();
}
}
4. Use Selectors, Not Direct State Access
// ✅ Correct
this.users$ = this.store.select(UserState.getUsers);
// ❌ Wrong
this.users$ = this.store.select(state => state.users.users);
5. Clear Error State After Handling
effect(() => {
const error = this.error();
if (error) {
this.showErrorNotification(error);
// Clear error after showing notification
this.store.dispatch(new ClearError());
}
});
6. Use Snapshots for Synchronous Access
When you need the current value immediately:
const currentUsers = this.store.selectSnapshot(UserState.getUsers);
const isLoading = this.store.selectSnapshot(UserState.isLoading);
if (!isLoading && currentUsers.length === 0) {
this.store.dispatch(new LoadUsers());
}
7. Optimize Re-renders with OnPush
All components use OnPush change detection - ensure immutability:
// ✅ Correct: creates new array
const updatedUsers = [...users, newUser];
// ❌ Wrong: mutates existing array
users.push(newUser);
Performance Optimization
1. Memoized Selectors
For expensive computations, create memoized selectors:
@Selector([UserState.getUsers])
static getActiveUsers(users: User[]): User[] {
return users.filter(u => u.status === 'active');
}
2. Lazy Loading
Load users only when needed:
export class UserListComponent implements OnInit {
private hasLoaded = false;
ngOnInit() {
const users = this.store.selectSnapshot(UserState.getUsers);
if (users.length === 0 && !this.hasLoaded) {
this.store.dispatch(new LoadUsers());
this.hasLoaded = true;
}
}
}
3. Debounce Search Input
Debounce search to reduce API calls:
searchControl = new FormControl('');
ngOnInit() {
this.searchControl.valueChanges
.pipe(
debounceTime(300),
distinctUntilChanged(),
tap(query => this.store.dispatch(new SetFilters({ search: query })))
)
.subscribe();
}
4. Virtual Scrolling
For large lists, use virtual scrolling:
import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
@Component({
template: `
<cdk-virtual-scroll-viewport itemSize="50" class="user-list">
@for (user of users(); track user.id) {
<lib-user-card [user]="user" />
}
</cdk-virtual-scroll-viewport>
`
})
5. Pagination Over Full Load
Load paginated data instead of all users:
loadPage(page: number, pageSize: number) {
this.store.dispatch(new LoadUsers({
...this.currentFilters,
pagination: { page, pageSize }
}));
}
Testing State
Unit Testing Actions
import { TestBed } from '@angular/core/testing';
import { Store, provideStore } from '@ngxs/store';
import { UserState } from './user.state';
describe('UserState', () => {
let store: Store;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [provideStore([UserState])]
});
store = TestBed.inject(Store);
});
it('should select user', async () => {
const user = { id: 'user-1', name: 'John' };
await store.dispatch(new SelectUser(user)).toPromise();
const selected = store.selectSnapshot(UserState.getSelectedUser);
expect(selected).toEqual(user);
});
});
Common Issues & Solutions
Issue: State not updating after action dispatch
Solution: Ensure you're returning new objects, not mutating:
// ✅ Correct
patchState({ users: [...users, newUser] });
// ❌ Wrong
users.push(newUser);
patchState({ users });
Issue: Component not re-rendering
Solution: Check OnPush change detection and signal/observable usage:
// ✅ Correct
users = toSignal(this.store.select(UserState.getUsers));
// In template
@for (user of users(); track user.id) { ... }
Issue: Memory leaks from subscriptions
Solution: Use takeUntil or async pipe:
private destroy$ = new Subject<void>();
ngOnInit() {
this.store.select(UserState.getUsers)
.pipe(takeUntil(this.destroy$))
.subscribe(users => { ... });
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
Resources
- NGXS Documentation: https://www.ngxs.io/
- Angular Signals: https://angular.dev/guide/signals
- RxJS Operators: https://rxjs.dev/api
- Event System Guide:
@libs-core/event-systemdocumentation