Saturday, August 16, 2025
0 comments

Master Angular 20 State Management Basics: A Beginner’s Guide to Service-Based State, RxJS BehaviorSubject, and a Shared State Demo with FreeAPI

 



Why State Management?State management involves organizing and maintaining the application’s data (state) to ensure components have access to consistent, up-to-date information. In Angular, state includes user data, UI states (e.g., loading indicators), form inputs, or API responses.Why It Matters
  • Consistency: Ensures all components reflect the same data (e.g., a selected user).
  • Scalability: Simplifies data management as the app grows.
  • Reactivity: Updates UI automatically when data changes.
  • Testability: Centralizes state logic for easier testing.
  • User Experience: Prevents inconsistencies (e.g., outdated user lists).
Common Scenarios
  • Sharing user selections across components (e.g., selecting a user in AdminComponent and displaying it in UserComponent).
  • Managing loading/error states for API calls.
  • Persisting form data or user preferences.

Service-Based State vs. External LibrariesAngular offers multiple approaches to state management, primarily service-based and external libraries.Service-Based State ManagementUsing Angular services with RxJS (e.g., BehaviorSubject) to manage state is simple and built into the framework.Pros:
  • Lightweight: No external dependencies.
  • Familiar: Leverages Angular services and RxJS, which you’re already using.
  • Flexible: Suitable for small to medium-sized apps.
  • Easy to Test: Services are injectable and testable.
Cons:
  • Limited Scalability: Can become complex in large apps with many states.
  • Manual Management: Requires careful handling of subscriptions and updates.
Example: Use a service to store and share the selected user state.External LibrariesLibraries like NgRx, Akita, or NGXS provide structured state management with advanced features.Pros:
  • Scalability: Handles complex state in large apps.
  • Predictability: Enforces patterns like Redux (e.g., actions, reducers).
  • Tooling: Includes dev tools for debugging state changes.
  • Immutability: Ensures predictable state updates.
Cons:
  • Learning Curve: Requires understanding new concepts (e.g., actions, selectors).
  • Boilerplate: More code for simple tasks.
  • Overhead: Adds dependencies and complexity for small apps.
Example: NgRx uses a store, actions, and reducers to manage global state.Recommendation for Beginners: Start with service-based state management using RxJS BehaviorSubject for simplicity. Transition to libraries like NgRx for large-scale apps.
Intro to RxJS BehaviorSubjectA BehaviorSubject is a type of RxJS Subject that:
  • Holds a single value (or initial value) and emits it to new subscribers.
  • Updates all subscribers when the value changes.
  • Is ideal for managing shared state reactively.
Key Features
  • Initial Value: Requires an initial value (e.g., null or an empty object).
  • Current Value: Accessible via getValue() or subscriptions.
  • Reactive Updates: Automatically notifies subscribers of changes.
Example
typescript
// src/app/services/state.service.ts
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class StateService {
  private userState = new BehaviorSubject<string | null>(null);
  userState$ = this.userState.asObservable();

  setUser(user: string | null) {
    this.userState.next(user);
  }

  getCurrentUser() {
    return this.userState.getValue();
  }
}
typescript
// src/app/components/example/example.component.ts
import { Component, OnInit } from '@angular/core';
import { StateService } from '../../services/state.service';

@Component({
  selector: 'app-example',
  template: `
    p: Current User: {{ user$ | async }}
    button: (click)="updateUser()": Update User
  `,
  standalone: true
})
export class ExampleComponent implements OnInit {
  user$ = this.stateService.userState$;

  constructor(private stateService: StateService) {}

  ngOnInit() {
    console.log('Current user:', this.stateService.getCurrentUser());
  }

  updateUser() {
    this.stateService.setUser('Alice');
  }
}
Explanation:
  • BehaviorSubject: Initializes with null and emits updates via next().
  • asObservable(): Exposes the read-only Observable for subscriptions.
  • async pipe: Renders the latest value in the template.

Hands-On Project: Small Shared State DemoLet’s extend your angular-routing-demo project to manage the selected user state using a BehaviorSubject in a new StateService. The AdminComponent will select a user, update the state, and persist it in local storage. The UserComponent will display the selected user reactively, building on the Module 8 setup with the freeapi.miniprojectideas.com API.Step 1: Set Up the ProjectEnsure the project is set up (from Module 8):
bash
ng new angular-routing-demo
cd angular-routing-demo
ng g component components/user
ng g component components/admin
ng g component components/user-details
ng g component components/not-found
ng g component components/admin-dashboard
ng g component components/admin-settings
ng g component components/register
ng g component components/login
ng g component components/user-info
ng g pipe pipes/filter-by-role
ng g service services/user
ng g service services/storage
ng g service services/state
Step 2: Create StateService
typescript
// src/app/services/state.service.ts
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { User } from '../models/user.model';
import { StorageService } from './storage.service';

@Injectable({
  providedIn: 'root'
})
export class StateService {
  private selectedUserSubject = new BehaviorSubject<User | null>(null);
  selectedUser$ = this.selectedUserSubject.asObservable();

  constructor(private storageService: StorageService) {
    const storedUser = this.storageService.getLocalStorage('selectedUser');
    if (storedUser) {
      this.selectedUserSubject.next(storedUser);
    }
  }

  setSelectedUser(user: User | null) {
    this.selectedUserSubject.next(user);
    if (user) {
      this.storageService.setLocalStorage('selectedUser', user);
    } else {
      this.storageService.clearLocalStorage('selectedUser');
    }
  }

  getCurrentUser() {
    return this.selectedUserSubject.getValue();
  }
}
Explanation:
  • BehaviorSubject<User | null>: Holds the selected user state, initialized as null.
  • selectedUser$: Exposes the state as an Observable.
  • Integrates with StorageService to persist state in local storage.
  • Loads stored user on initialization.
Step 3: Update User Model
typescript
// src/app/models/user.model.ts
export interface User {
  id: number;
  userName: string;
  emailId: string;
  role: string;
}
Step 4: Update StorageService
typescript
// src/app/services/storage.service.ts
import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class StorageService {
  setLocalStorage(key: string, value: any) {
    localStorage.setItem(key, JSON.stringify(value));
  }

  getLocalStorage(key: string) {
    const data = localStorage.getItem(key);
    return data ? JSON.parse(data) : null;
  }

  clearLocalStorage(key: string) {
    localStorage.removeItem(key);
  }
}
Step 5: Update UserService
typescript
// src/app/services/user.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { catchError, finalize } from 'rxjs/operators';
import { User } from '../models/user.model';

@Injectable({
  providedIn: 'root'
})
export class UserService {
  private apiUrl = 'https://freeapi.miniprojectideas.com/api/User/GetAllUsers';
  private localUsers: User[] = [
    { id: 1, userName: 'Alice', emailId: 'alice@example.com', role: 'admin' },
    { id: 2, userName: 'Bob', emailId: 'bob@example.com', role: 'user' },
    { id: 3, userName: 'Charlie', emailId: 'charlie@example.com', role: 'admin' },
    { id: 4, userName: 'David', emailId: 'david@example.com', role: 'user' }
  ];
  private admins = [
    { username: 'admin1', password: 'admin123' },
    { username: 'admin2', password: 'admin456' }
  ];

  constructor(private http: HttpClient) {}

  getUsers(): Observable<User[]> {
    return this.http.get<User[]>(this.apiUrl).pipe(
      catchError(error => {
        console.error('Error fetching users from API:', error);
        return of(this.localUsers);
      })
    );
  }

  getUserById(id: number): Observable<User | undefined> {
    return this.http.get<User>(`${this.apiUrl}/${id}`).pipe(
      catchError(error => {
        console.error('Error fetching user by ID:', error);
        const localUser = this.localUsers.find(user => user.id === id);
        return of(localUser);
      })
    );
  }

  addUser(user: User): Observable<any> {
    return this.http.post(this.apiUrl, user).pipe(
      catchError(error => {
        console.error('Error adding user:', error);
        this.localUsers.push(user);
        return of(user);
      })
    );
  }

  validateAdmin(username: string, password: string): boolean {
    return this.admins.some(admin => admin.username === username && admin.password === password);
  }
}
Step 6: Update app.routes.ts
typescript
// src/app/app.routes.ts
import { Routes } from '@angular/router';
import { UserComponent } from './components/user/user.component';
import { AdminComponent } from './components/admin/admin.component';
import { UserDetailsComponent } from './components/user-details/user-details.component';
import { NotFoundComponent } from './components/not-found/not-found.component';
import { AdminDashboardComponent } from './components/admin-dashboard/admin-dashboard.component';
import { AdminSettingsComponent } from './components/admin-settings/admin-settings.component';
import { RegisterComponent } from './components/register/register.component';
import { LoginComponent } from './components/login/login.component';

export const routes: Routes = [
  {
    path: 'user',
    component: UserComponent,
    children: [
      { path: ':id', component: UserDetailsComponent }
    ]
  },
  { path: 'register', component: RegisterComponent },
  { path: 'login', component: LoginComponent },
  {
    path: 'admin',
    component: AdminComponent,
    children: [
      { path: 'dashboard', component: AdminDashboardComponent },
      { path: 'settings', component: AdminSettingsComponent },
      { path: '', redirectTo: 'dashboard', pathMatch: 'full' }
    ]
  },
  { path: '', redirectTo: '/user', pathMatch: 'full' },
  { path: '**', component: NotFoundComponent }
];
Step 7: Update main.ts
typescript
// src/main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { AppComponent } from './app/app.component';
import { routes } from './app/app.routes';

bootstrapApplication(AppComponent, {
  providers: [provideRouter(routes), provideHttpClient()]
}).catch(err => console.error(err));
Step 8: Update AppComponent
typescript
// src/app/app.component.ts
import { Component, OnInit } from '@angular/core';
import { Router, NavigationStart, NavigationEnd, RouterOutlet, RouterLink } from '@angular/router';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
  standalone: true,
  imports: [RouterOutlet, RouterLink]
})
export class AppComponent implements OnInit {
  constructor(private router: Router) {}

  ngOnInit() {
    this.router.events.subscribe(event => {
      if (event instanceof NavigationStart) {
        console.log('Navigation started:', event.url);
      } else if (event instanceof NavigationEnd) {
        console.log('Navigation ended:', event.url);
      }
    });
  }
}
typescript
// src/app/app.component.html
nav:
  a: routerLink="/user" routerLinkActive="active": User Dashboard
  a: routerLink="/register" routerLinkActive="active": Register
  a: routerLink="/login" routerLinkActive="active": Login
  a: routerLink="/admin" routerLinkActive="active": Admin Panel
router-outlet
css
// src/app/app.component.css
nav { margin: 20px; }
a { margin-right: 10px; text-decoration: none; }
.active { font-weight: bold; color: #007bff; }
Step 9: Update AdminComponentUse StateService to manage the selected user state.
typescript
// src/app/components/admin/admin.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { RouterOutlet, RouterLink, Router } from '@angular/router';
import { Observable, of } from 'rxjs';
import { catchError, finalize } from 'rxjs/operators';
import { FilterByRolePipe } from '../../pipes/filter-by-role.pipe';
import { UserService } from '../../services/user.service';
import { StateService } from '../../services/state.service';
import { User } from '../../models/user.model';

@Component({
  selector: 'app-admin',
  templateUrl: './admin.component.html',
  styleUrls: ['./admin.component.css'],
  standalone: true,
  imports: [FormsModule, RouterOutlet, RouterLink, FilterByRolePipe]
})
export class AdminComponent implements OnInit, OnDestroy {
  users$: Observable<User[]>;
  filterRole = 'admin';
  isHighlighted = false;
  isLoading = false;
  error: string | null = null;
  selectedUser$ = this.stateService.selectedUser$;

  constructor(
    private userService: UserService,
    private stateService: StateService,
    private router: Router
  ) {}

  ngOnInit() {
    this.isLoading = true;
    this.users$ = this.userService.getUsers().pipe(
      catchError(err => {
        this.error = 'Failed to load users: ' + err.message;
        this.isLoading = false;
        return of([]);
      }),
      finalize(() => this.isLoading = false)
    );
  }

  ngOnDestroy() {
    console.log('AdminComponent: Destroyed');
  }

  toggleHighlight() {
    this.isHighlighted = !this.isHighlighted;
  }

  setFilterRole(role: string) {
    this.filterRole = role;
  }

  logSearch(value: string) {
    console.log('Search:', value);
  }

  selectUser(user: User) {
    this.stateService.setSelectedUser(user);
    this.router.navigate(['/user']);
  }

  clearSelectedUser() {
    this.stateService.setSelectedUser(null);
  }
}
typescript
// src/app/components/admin/admin.component.html
h2: Admin Panel
nav:
  a: routerLink="dashboard" routerLinkActive="active": Dashboard
  a: routerLink="settings" routerLinkActive="active": Settings
router-outlet
h3: Admin Users List
div: *ngIf="error" class="error": {{ error }}
div: *ngIf="isLoading" class="loading": Loading users...
div: *ngIf="users$ | async as users; else loading":
  div:
    label: Filter by Role: 
    select: [(ngModel)]="filterRole" (change)="setFilterRole(filterRole)":
      option: value="all": All
      option: value="admin": Admin
      option: value="user": User
  button: (click)="toggleHighlight()": Toggle Highlight
  ul:
    li: *ngFor="let user of users | filterByRole:filterRole" [ngClass]="{'highlight': isHighlighted && user.role === 'admin'}":
      {{ user.userName | uppercase }} ({{ user.role }}) - {{ user.emailId }}
      button: (click)="selectUser(user)": Select
div: *ngIf="selectedUser$ | async as selectedUser":
  p: Selected: {{ selectedUser.userName }}
  button: (click)="clearSelectedUser()": Clear Selection
input: #searchInput placeholder="Search users"
button: (click)="logSearch(searchInput.value)": Search
ng-template: #loading: Loading users...
css
// src/app/components/admin/admin.component.css
ul { list-style-type: none; padding: 0; }
li { padding: 10px; margin: 5px 0; border: 1px solid #ccc; }
.highlight { background-color: #e0f7fa; }
nav { margin: 10px 0; }
a { margin-right: 10px; text-decoration: none; }
.active { font-weight: bold; color: #007bff; }
.loading { text-align: center; color: #007bff; }
.error { text-align: center; color: red; }
button { margin-left: 10px; }
Step 10: Update UserComponentUse StateService to display the selected user reactively.
typescript
// src/app/components/user/user.component.ts
import { Component, OnInit } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { Observable, of } from 'rxjs';
import { catchError, finalize } from 'rxjs/operators';
import { UserService } from '../../services/user.service';
import { StateService } from '../../services/state.service';
import { UserInfoComponent } from '../user-info/user-info.component';
import { User } from '../../models/user.model';

@Component({
  selector: 'app-user',
  templateUrl: './user.component.html',
  styleUrls: ['./user.component.css'],
  standalone: true,
  imports: [FormsModule, RouterLink, UserInfoComponent]
})
export class UserComponent implements OnInit {
  searchQuery = '';
  users$: Observable<User[]>;
  isLoading = false;
  error: string | null = null;
  selectedUser$ = this.stateService.selectedUser$;

  constructor(
    private route: ActivatedRoute,
    private router: Router,
    private userService: UserService,
    private stateService: StateService
  ) {}

  ngOnInit() {
    this.isLoading = true;
    this.users$ = this.userService.getUsers().pipe(
      catchError(err => {
        this.error = 'Failed to load users: ' + err.message;
        this.isLoading = false;
        return of([]);
      }),
      finalize(() => this.isLoading = false)
    );
    this.route.queryParamMap.subscribe(params => {
      this.searchQuery = params.get('search') || '';
    });
  }

  updateQuery() {
    this.router.navigate(['/user'], { queryParams: { search: this.searchQuery } });
  }

  clearSelectedUser() {
    this.stateService.setSelectedUser(null);
  }
}
typescript
// src/app/components/user/user.component.html
h2: User Dashboard
div: *ngIf="error" class="error": {{ error }}
div: *ngIf="isLoading" class="loading": Loading users...
div:
  label: Search users: 
  input: [(ngModel)]="searchQuery" placeholder="Search users"
  button: (click)="updateQuery()": Search
p: Search Query: {{ searchQuery }}
app-user-info: [user]="selectedUser$ | async" (clearUser)="clearSelectedUser()"
router-outlet
ul: *ngIf="users$ | async as users; else loading":
  li: *ngFor="let user of users":
    a: routerLink="/user/{{ user.id }}": {{ user.userName }} ({{ user.role }}) - {{ user.emailId }}
ng-template: #loading: Loading users...
css
// src/app/components/user/user.component.css
ul { list-style-type: none; padding: 0; }
li { padding: 10px; margin: 5px 0; border: 1px solid #ccc; }
.loading { text-align: center; color: #007bff; }
.error { text-align: center; color: red; }
Step 11: Update UserInfoComponent
typescript
// src/app/components/user-info/user-info.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { User } from '../../models/user.model';
import { StateService } from '../../services/state.service';

@Component({
  selector: 'app-user-info',
  template: `
    div: *ngIf="user":
      p: Selected User: {{ user.userName }} ({{ user.role }})
      button: (click)="clearSelection()": Clear Selection
  `,
  standalone: true
})
export class UserInfoComponent {
  @Input() user: User | null = null;
  @Output() clearUser = new EventEmitter<void>();

  constructor(private stateService: StateService) {}

  clearSelection() {
    this.stateService.setSelectedUser(null);
    this.clearUser.emit();
  }
}
Step 12: Update UserDetailsComponent
typescript
// src/app/components/user-details/user-details.component.ts
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, RouterLink } from '@angular/router';
import { Observable, of } from 'rxjs';
import { catchError, finalize } from 'rxjs/operators';
import { UserService } from '../../services/user.service';
import { User } from '../../models/user.model';

@Component({
  selector: 'app-user-details',
  templateUrl: './user-details.component.html',
  styleUrls: ['./user-details.component.css'],
  standalone: true,
  imports: [RouterLink]
})
export class UserDetailsComponent implements OnInit {
  user$: Observable<User | undefined>;
  isLoading = false;
  error: string | null = null;

  constructor(
    private route: ActivatedRoute,
    private userService: UserService
  ) {}

  ngOnInit() {
    this.isLoading = true;
    this.route.paramMap.subscribe(params => {
      const id = Number(params.get('id'));
      this.user$ = this.userService.getUserById(id).pipe(
        catchError(err => {
          this.error = 'Failed to load user: ' + err.message;
          this.isLoading = false;
          return of(undefined);
        }),
        finalize(() => this.isLoading = false)
      );
    });
  }
}
typescript
// src/app/components/user-details/user-details.component.html
h2: User Details
div: *ngIf="error" class="error": {{ error }}
div: *ngIf="isLoading" class="loading": Loading user...
div: *ngIf="user$ | async as user; else noUser":
  p: Name: {{ user.userName }}
  p: Email: {{ user.emailId }}
  p: Role: {{ user.role | uppercase }}
p: else noUser: User not found
p: Back to User Dashboard
css
// src/app/components/user-details/user-details.component.css
.loading { text-align: center; color: #007bff; }
.error { text-align: center; color: red; }
Step 13: Update RegisterComponent
typescript
// src/app/components/register/register.component.ts
import { Component } from '@angular/core';
import { ReactiveFormsModule, FormGroup, FormControl, Validators } from '@angular/forms';
import { Router, RouterLink } from '@angular/router';
import { UserService } from '../../services/user.service';
import { noSpacesValidator } from '../../validators/no-spaces.validator';

@Component({
  selector: 'app-register',
  templateUrl: './register.component.html',
  styleUrls: ['./register.component.css'],
  standalone: true,
  imports: [ReactiveFormsModule, RouterLink]
})
export class RegisterComponent {
  registerForm = new FormGroup({
    userName: new FormControl('', [Validators.required, Validators.minLength(3), noSpacesValidator()]),
    emailId: new FormControl('', [Validators.required, Validators.email]),
    role: new FormControl('user', [Validators.required])
  });
  isLoading = false;
  error: string | null = null;

  constructor(private userService: UserService, private router: Router) {}

  onSubmit() {
    if (this.registerForm.valid) {
      this.isLoading = true;
      const formValue = this.registerForm.value;
      const newUser = {
        id: this.userService.getUsers().length + 1,
        userName: formValue.userName || '',
        emailId: formValue.emailId || '',
        role: formValue.role || 'user'
      };
      this.userService.addUser(newUser).subscribe({
        next: () => {
          this.registerForm.reset();
          this.router.navigate(['/user']);
        },
        error: err => {
          this.error = 'Failed to register user: ' + err.message;
          this.isLoading = false;
        },
        complete: () => {
          this.isLoading = false;
        }
      });
    }
  }
}
typescript
// src/app/components/register/register.component.html
h2: User Registration
div: *ngIf="error" class="error": {{ error }}
div: *ngIf="isLoading" class="loading": Registering user...
form: [formGroup]="registerForm" (ngSubmit)="onSubmit()":
  div:
    label: Username: 
    input: formControlName="userName"
    div: *ngIf="registerForm.get('userName')?.hasError('required') && registerForm.get('userName')?.touched":
      Username is required
    div: *ngIf="registerForm.get('userName')?.hasError('minlength') && registerForm.get('userName')?.touched":
      Username must be at least 3 characters
    div: *ngIf="registerForm.get('userName')?.hasError('noSpaces') && registerForm.get('userName')?.touched":
      Username cannot contain spaces
  div:
    label: Email: 
    input: formControlName="emailId"
    div: *ngIf="registerForm.get('emailId')?.hasError('required') && registerForm.get('emailId')?.touched":
      Email is required
    div: *ngIf="registerForm.get('emailId')?.hasError('email') && registerForm.get('emailId')?.touched":
      Invalid email format
  div:
    label: Role: 
    select: formControlName="role":
      option: value="user": User
      option: value="admin": Admin
  button: type="submit" [disabled]="registerForm.invalid || isLoading": Register
p: Back to User Dashboard
css
// src/app/components/register/register.component.css
form { margin: 20px; padding: 20px; border: 1px solid #ccc; }
input, select { padding: 5px; margin: 5px; width: 200px; }
div { margin-bottom: 10px; }
.error { color: red; font-size: 0.9em; }
.loading { text-align: center; color: #007bff; }
Step 14: Update LoginComponent
typescript
// src/app/components/login/login.component.ts
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { Router, RouterLink } from '@angular/router';
import { UserService } from '../../services/user.service';

@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.css'],
  standalone: true,
  imports: [FormsModule, RouterLink]
})
export class LoginComponent {
  username = '';
  password = '';
  errorMessage = '';

  constructor(private userService: UserService, private router: Router) {}

  onSubmit() {
    if (this.userService.validateAdmin(this.username, this.password)) {
      this.router.navigate(['/admin']);
    } else {
      this.errorMessage = 'Invalid username or password';
    }
  }
}
typescript
// src/app/components/login/login.component.html
h2: Admin Login
form: (ngSubmit)="onSubmit()":
  div:
    label: Username: 
    input: [(ngModel)]="username" name="username" required
    div: *ngIf="form.username?.errors?.['required'] && form.username?.touched": Username is required
  div:
    label: Password: 
    input: [(ngModel)]="password" name="password" type="password" required
    div: *ngIf="form.password?.errors?.['required'] && form.password?.touched": Password is required
  div: *ngIf="errorMessage" class="error": {{ errorMessage }}
  button: type="submit" [disabled]="!form.valid": Login
p: Back to User Dashboard
css
// src/app/components/login/login.component.css
form { margin: 20px; padding: 20px; border: 1px solid #ccc; }
input { padding: 5px; margin: 5px; width: 200px; }
div { margin-bottom: 10px; }
.error { color: red; font-size: 0.9em; }
Step 15: Update Child Components
typescript
// src/app/components/admin-dashboard/admin-dashboard.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-admin-dashboard',
  template: `
    h3: Admin Dashboard
    p: Welcome to the Admin Dashboard!
  `,
  standalone: true
})
export class AdminDashboardComponent {}
typescript
// src/app/components/admin-settings/admin-settings.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-admin-settings',
  template: `
    h3: Admin Settings
    p: Configure settings here.
  `,
  standalone: true
})
export class AdminSettingsComponent {}
Step 16: Update NotFoundComponent
typescript
// src/app/components/not-found/not-found.component.ts
import { Component } from '@angular/core';
import { RouterLink } from '@angular/router';

@Component({
  selector: 'app-not-found',
  template: `
    h2: 404 - Page Not Found
    p: Go back to User Dashboard
  `,
  standalone: true,
  imports: [RouterLink]
})
export class NotFoundComponent {}
Step 17: Update FilterByRolePipe
typescript
// src/app/pipes/filter-by-role.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';
import { User } from '../models/user.model';

@Pipe({
  name: 'filterByRole',
  standalone: true
})
export class FilterByRolePipe implements PipeTransform {
  transform(users: User[], role: string): User[] {
    if (!users || role === 'all') return users;
    return users.filter(user => user.role === role);
  }
}
Step 18: Update no-spaces.validator.ts
typescript
// src/app/validators/no-spaces.validator.ts
import { AbstractControl, ValidatorFn } from '@angular/forms';

export function noSpacesValidator(): ValidatorFn {
  return (control: AbstractControl): { [key: string]: any } | null => {
    const hasSpaces = control.value?.includes(' ');
    return hasSpaces ? { noSpaces: true } : null;
  };
}
Step 19: Test the Application
  1. Run ng serve and visit http://localhost:4200.
  2. Test features:
    • Admin Panel (/admin):
      • Fetch users from freeapi.miniprojectideas.com.
      • Select a user (click “Select”) and verify it updates in UserInfoComponent and local storage (check DevTools > Application > Local Storage).
      • Clear the selection and verify the state updates reactively.
    • User Dashboard (/user):
      • View the selected user in UserInfoComponent reactively via selectedUser$ | async.
      • Clear the selection and confirm it updates across components.
    • Register (/register):
      • Add a new user and verify it appears in User and Admin lists.
    • Login (/login):
      • Log in with admin1/admin123.
    • User Details (/user/:id):
      • View user details.
    • 404 Page (/invalid):
      • Confirm the Not Found page appears.

Troubleshooting Common ErrorsTS2305: Module '"./app/app"' has no exported member 'App'Cause: Incorrect import in main.ts.Fix:
typescript
// src/main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { provideRouter, provideHttpClient } from '@angular/router';
import { AppComponent } from './app/app.component';
import { routes } from './app/app.routes';

bootstrapApplication(AppComponent, {
  providers: [provideRouter(routes), provideHttpClient()]
}).catch(err => console.error(err));
Steps:
  1. Ensure AppComponent is exported in app.component.ts.
  2. Verify file paths.
  3. Run ng serve.
BehaviorSubject Not UpdatingCause: Incorrect usage of next() or subscription issues.Fix:
  • Ensure next() is called with the new value in StateService.
  • Verify components subscribe to selectedUser$ using async pipe or subscribe().
Local Storage Sync IssuesCause: Mismatch between BehaviorSubject and local storage.Fix:
  • Ensure setSelectedUser updates both the BehaviorSubject and local storage.
  • Check JSON.stringify and JSON.parse for correct serialization.

Best Practices for State Management
  • Use BehaviorSubject for Simple State: Ideal for small, reactive state needs.
  • Centralize State: Keep state logic in a single service (e.g., StateService).
  • Avoid Over-Subscription: Use async pipe to manage subscriptions automatically.
  • Type Safety: Use interfaces (e.g., User) for state data.
  • Combine with Storage: Persist critical state in local storage for durability.
  • Consider Libraries for Large Apps: Evaluate NgRx or Akita for complex state needs.

0 comments:

Featured Post

Master Angular 20 Basics: A Complete Beginner’s Guide with Examples and Best Practices

Welcome to the complete Angular 20 learning roadmap ! This series takes you step by step from basics to intermediate concepts , with hands...

Subscribe

 
Toggle Footer
Top