State Management in Angular

State Management in Angular

Question 1: What are the different approaches to state management in Angular?

Answer: Angular offers several state management approaches:

  1. Signals (New in Angular 17+)
  2. Services with RxJS
  3. NgRx (Redux pattern)
  4. NGXS
  5. Component State
  6. Local Storage/Session Storage

Question 2: How do you implement state management using Signals?

Answer: Here’s a modern implementation using Signals:

// user.model.ts
interface User {
  id: string;
  name: string;
  email: string;
}

// user.service.ts
@Injectable({ providedIn: 'root' })
export class UserStateService {
  // State definition
  private userState = signal<User[]>([]);
  private selectedUserState = signal<User | null>(null);
  private loadingState = signal(false);
  
  // Computed values
  readonly users = computed(() => this.userState());
  readonly selectedUser = computed(() => this.selectedUserState());
  readonly isLoading = computed(() => this.loadingState());
  
  // Actions
  async loadUsers() {
    this.loadingState.set(true);
    try {
      const users = await this.api.getUsers();
      this.userState.set(users);
    } finally {
      this.loadingState.set(false);
    }
  }
  
  selectUser(id: string) {
    const user = this.userState()
      .find(u => u.id === id);
    this.selectedUserState.set(user || null);
  }
  
  updateUser(updatedUser: User) {
    this.userState.update(users => 
      users.map(user => 
        user.id === updatedUser.id ? updatedUser : user
      )
    );
  }
}

// user-list.component.ts
@Component({
  selector: 'app-user-list',
  template: `
    @if (isLoading()) {
      <loading-spinner />
    } @else {
      <ul>
        @for (user of users(); track user.id) {
          <li (click)="selectUser(user.id)">
            {{ user.name }}
          </li>
        }
      </ul>
    }
  `
})
export class UserListComponent {
  private userState = inject(UserStateService);
  
  users = this.userState.users;
  isLoading = this.userState.isLoading;
  
  selectUser(id: string) {
    this.userState.selectUser(id);
  }
}

Question 3: How do you implement state management with NgRx?

Answer: Here’s a comprehensive NgRx implementation:

// State interface
interface AppState {
  users: UserState;
}

interface UserState {
  users: User[];
  selectedUser: User | null;
  loading: boolean;
  error: string | null;
}

// Actions
const loadUsers = createAction('[Users] Load');
const loadUsersSuccess = createAction(
  '[Users] Load Success',
  props<{ users: User[] }>()
);
const loadUsersFailure = createAction(
  '[Users] Load Failure',
  props<{ error: string }>()
);

// can also do like below with latest version of NGRX
export const userActions = createActions({
  loadUsers: emptyProps(),
  loadUsersSuccess: props<{ users: User[] }>(),
  loadUsersFailure: props<{ error: string }>()
})

// Reducer
const initialState: UserState = {
  users: [],
  selectedUser: null,
  loading: false,
  error: null
};

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

// Effects
@Injectable()
export class UserEffects {
  loadUsers$ = createEffect(() => 
    this.actions$.pipe(
      ofType(loadUsers),
      switchMap(() => 
        this.userService.getUsers().pipe(
          map(users => loadUsersSuccess({ users })),
          catchError(error => 
            of(loadUsersFailure({ error: error.message }))
          )
        )
      )
    )
  );
  
  constructor(
    private actions$: Actions,
    private userService: UserService
  ) {}
}

// Selectors
const selectUserState = (state: AppState) => state.users;

export const selectAllUsers = createSelector(
  selectUserState,
  state => state.users
);

export const selectLoading = createSelector(
  selectUserState,
  state => state.loading
);

// Component
@Component({
  selector: 'app-user-list',
  template: `
    @if (loading$ | async) {
      <loading-spinner />
    }
    
    @if (error$ | async; as error) {
      <error-alert [message]="error" />
    }
    
    @if (users$ | async; as users) {
      <ul>
        @for (user of users; track user.id) {
          <li>{{ user.name }}</li>
        }
      </ul>
    }
  `
})
export class UserListComponent {
  private store = inject(Store);
  
  users$ = this.store.select(selectAllUsers);
  loading$ = this.store.select(selectLoading);
  error$ = this.store.select(selectError);
  
  ngOnInit() {
    this.store.dispatch(loadUsers());
  }
}

Question 4: How do you handle component-level state?

Answer: Here’s how to manage component state effectively:

@Component({
  selector: 'app-user-form',
  template: `
    <form [formGroup]="form">
      <input formControlName="name" />
      <input formControlName="email" />
      
      @if (showValidationErrors()) {
        <div class="errors">
          @for (error of validationErrors(); track error) {
            <p class="error">{{ error }}</p>
          }
        </div>
      }
      
      <button 
        [disabled]="isSubmitting()" 
        (click)="submit()"
      >
        Submit
      </button>
    </form>
  `
})
export class UserFormComponent {
  // Form state
  form = new FormGroup({
    name: new FormControl(''),
    email: new FormControl('')
  });
  
  // UI state
  showValidationErrors = signal(false);
  validationErrors = signal<string[]>([]);
  isSubmitting = signal(false);
  
  // Computed state
  isValid = computed(() => 
    this.form.valid && !this.isSubmitting()
  );
  
  async submit() {
    if (!this.isValid()) return;
    
    this.isSubmitting.set(true);
    try {
      await this.userService.save(this.form.value);
    } catch (error) {
      this.handleError(error);
    } finally {
      this.isSubmitting.set(false);
    }
  }
  
  private handleError(error: any) {
    this.showValidationErrors.set(true);
    this.validationErrors.set(
      this.extractErrors(error)
    );
  }
}

Interview Tips 💡

  1. State Management Patterns

    // Service with State Pattern
    @Injectable({ providedIn: 'root' })
    export class StateService<T> {
      private state = signal<T>(initialState);
      
      select<K>(selector: (state: T) => K) {
        return computed(() => selector(this.state()));
      }
      
      update(updater: (state: T) => T) {
        this.state.update(updater);
      }
    }
  2. Performance Optimization

    // Memoization with computed
    @Injectable()
    export class OptimizedService {
      private users = signal<User[]>([]);
      
      // Memoized computation
      readonly activeUsers = computed(() => 
        this.users().filter(user => user.isActive)
      );
      
      // Derived state
      readonly userCount = computed(() => 
        this.users().length
      );
    }
  3. Testing State

    describe('UserState', () => {
      let service: UserStateService;
      
      beforeEach(() => {
        TestBed.configureTestingModule({
          providers: [UserStateService]
        });
        service = TestBed.inject(UserStateService);
      });
      
      it('should update state', () => {
        service.updateUser(mockUser);
        expect(service.users()).toContain(mockUser);
      });
    });
  4. Common Patterns

    // Command Pattern
    interface Command {
      execute(): void;
      undo(): void;
    }
    
    class UpdateUserCommand implements Command {
      constructor(
        private state: UserState,
        private update: Partial<User>
      ) {}
      
      execute() {
        this.previousState = this.state.value;
        this.state.update(this.update);
      }
      
      undo() {
        this.state.set(this.previousState);
      }
    }
  5. Error Handling

    @Injectable()
    export class ErrorState {
      private errorState = signal<Error | null>(null);
      
      setError(error: Error) {
        this.errorState.set(error);
        setTimeout(() => 
          this.errorState.set(null), 
          5000
        );
      }
    }
  6. Best Practices

    // Immutable Updates
    this.state.update(state => ({
      ...state,
      users: [...state.users, newUser]
    }));
    
    // State Initialization
    @Injectable()
    export class AppInitializer {
      constructor(private store: Store) {}
      
      async initialize() {
        await this.loadInitialState();
        this.setupStateSync();
      }
    }

Remember: In interviews, focus on:

  • Different state management approaches
  • When to use each approach
  • Performance implications
  • Testing strategies
  • Error handling
  • Best practices
  • Real-world scenarios

Key points to emphasize:

  1. Signals vs NgRx vs Services
  2. State immutability
  3. Performance optimization
  4. Testing approaches
  5. Error handling
  6. State synchronization
  7. Best practices