A robust application provides clear feedback to its users. In a web application, this often means indicating when data is being loaded or when an error has occurred during an API call. We’ve already laid some groundwork for this in our UserService by using BehaviorSubject for loadingUsers and errorLoadingUsers. In this chapter, we’ll ensure these states are properly reflected in our UI for a better user experience.

This chapter will reinforce the use of signals for UI state management and demonstrate how observables from a service can drive component signals.

Reviewing UserService’s State Management

Recall our UserService (src/app/core/services/user.service.ts):

// src/app/core/services/user.service.ts (relevant snippets)
// ...
  loadingUsers = new BehaviorSubject<boolean>(false);
  errorLoadingUsers = new BehaviorSubject<string | null>(null);

  fetchUsers(): void {
    this.loadingUsers.next(true); // Set loading to true
    this.errorLoadingUsers.next(null); // Clear previous errors
    this.http.get<User[]>(this.apiUrl)
      .pipe(
        // ...
        tap((users) => {
          this._users.next(users);
          this.loadingUsers.next(false); // Set loading to false on success
        }),
        catchError(this.handleError) // Error handling pipe
      )
      .subscribe({
        next: () => console.log('Users fetched and updated.'),
        error: (err) => {
          this.errorLoadingUsers.next('Failed to load users. Please try again.'); // Set error message on failure
          this.loadingUsers.next(false); // Set loading to false on failure
          console.error('Error fetching users:', err);
        }
      });
  }

  addUser(newUser: NewUser): Observable<User> {
    this.loadingUsers.next(true); // Set loading to true for add operation
    return this.http.post<User>(this.apiUrl, newUser)
      .pipe(
        tap((createdUser) => {
          // ...
          this.loadingUsers.next(false); // Set loading to false on success
        }),
        catchError(this.handleError) // Error handling pipe
      );
  }
// ...

The UserService already correctly updates loadingUsers and errorLoadingUsers BehaviorSubjects. Our components now need to consume these effectively.

Step 1: UserListComponent - Ensure Loading/Error Display

We’ve already implemented the UI for loading and error states in UserListComponent in Chapter 15.2. Let’s quickly review and confirm it’s correctly wired up.

Component Logic (user-list.component.ts):

// src/app/features/users/components/user-list/user-list.component.ts (relevant snippets)
// ...
import { Subscription } from 'rxjs'; // Already there

export class UserListComponent implements OnInit, OnDestroy {
  // ...
  users = signal<User[]>([]);
  loading = signal(false); // Local signal to track loading state
  error = signal<string | null>(null); // Local signal to track error message

  ngOnInit(): void {
    this.subscriptions.add(
      this.userService.users$.subscribe((latestUsers) => {
        this.users.set(latestUsers);
      })
    );

    // Subscribing to service's loading state and updating local signal
    this.subscriptions.add(
      this.userService.loadingUsers.subscribe((isLoading) => {
        this.loading.set(isLoading);
      })
    );

    // Subscribing to service's error state and updating local signal
    this.subscriptions.add(
      this.userService.errorLoadingUsers.subscribe((err) => {
        this.error.set(err);
      })
    );
  }
// ...

Template (user-list.component.html):

<!-- src/app/features/users/components/user-list/user-list.component.html (relevant snippets) -->
  <!-- Loading State -->
  @if (loading()) {
    <div class="loading-message">...</div>
  }

  <!-- Error State -->
  @if (!loading() && error()) {
    <div class="error-message">...</div>
  }

  <!-- User List (only if not loading and no error) -->
  @if (!loading() && !error() && users().length > 0) {
    <ul class="user-cards">...</ul>
  }

This setup is correct! The component subscribes to the BehaviorSubjects from UserService and updates its local signals. These signals then drive the @if blocks in the template. This means that whenever UserService updates its loading or error status, UserListComponent will react and update its display.

Step 2: UserFormComponent - Enhance Loading/Error Display

Our UserFormComponent also interacts with the UserService (specifically addUser). We should ensure it provides feedback during the add operation and for any errors.

Component Logic (user-form.component.ts):

We already added feedbackMessage and feedbackIsError signals. Let’s make sure the “Add User” button correctly uses the service’s loading state.

// src/app/features/users/components/user-form/user-form.component.ts (relevant snippets)
// ...
export class UserFormComponent {
  private userService = inject(UserService);
  private router = inject(Router);
  private destroy$ = new Subject<void>();

  // ... other signals

  // Use a computed signal for button disabled state, combining form validity AND service loading
  isAddingUser = computed(() => this.userService.loadingUsers.getValue());
  // Note: userService.loadingUsers is a BehaviorSubject, so .getValue() is correct here.
  // If it were a signal, it would be userService.loadingUsers()

  // ... onSubmit() method ...
  onSubmit(): void {
    if (this.userForm.valid()) {
      const newUser: NewUser = this.userForm.value();
      this.feedbackMessage.set(null);
      this.feedbackIsError.set(false);

      this.userService.addUser(newUser)
        .pipe(takeUntil(this.destroy$))
        .subscribe({
          next: (addedUser) => {
            this.feedbackMessage.set(`User "${addedUser.name}" added successfully!`);
            this.feedbackIsError.set(false);
            this.userForm.reset();
            // We already added this:
            // setTimeout(() => this.router.navigate(['/users']), 1500); // Navigate after a short delay
          },
          error: (err) => {
            this.feedbackMessage.set(`Failed to add user: ${err.message || 'Unknown error'}`);
            this.feedbackIsError.set(true);
          },
        });
    } else {
      // ...
    }
  }
// ...

Template (user-form.component.html):

The submit button should already be disabled if userService.loadingUsers is true.

<!-- src/app/features/users/components/user-form/user-form.component.html (relevant snippets) -->
    <!-- Form Feedback -->
    @if (feedbackMessage()) {
      <div class="form-feedback" [class.error]="feedbackIsError()">
        <p>{{ feedbackMessage() }}</p>
      </div>
    }

    <div class="form-actions">
      <!-- Button is disabled if form invalid OR if user service is currently loading/adding -->
      <button type="submit" [disabled]="userForm.invalid() || isAddingUser()">
        @if (isAddingUser()) {
          Adding User...
        } @else {
          Add User
        }
      </button>
      <button type="button" (click)="userForm.reset()">Reset Form</button>
    </div>

Explanation:

  • isAddingUser Computed Signal: This computed signal directly reflects the userService.loadingUsers.getValue() state. This provides a clean way to react to the service’s loading status.
  • Submit Button Text: We use an @if block directly in the button content to show “Adding User…” when isAddingUser() is true, providing instant feedback.
  • Submit Button disabled Attribute: The button is disabled if userForm.invalid() OR if isAddingUser() is true, preventing multiple submissions.

Step 3: Run and Test Enhanced States

Ensure your json-server is running in one terminal:

npm run serve:json-api

And your Angular app in another:

ng serve

Open your browser to http://localhost:4200.

  • User List Page (/users):

    • Observe the initial loading state (it might be very brief).
    • Click “Refresh All” button. You should see the loading spinner briefly.
    • Simulate Network Delay/Error:
      • Open your browser’s Developer Tools (F12).
      • Go to the “Network” tab.
      • Find the “Throttling” dropdown (usually “No throttling”).
      • Select a slower option like “Fast 3G” or “Slow 3G”.
      • Click “Refresh All” again. You should see the “Loading users…” message with the spinner for a longer duration.
      • Now, stop your json-server. Click “Refresh All”. You should see the “Failed to load users…” error message with the “Try Again” button. Restart json-server and click “Try Again” to confirm recovery.
  • Add User Page (/users/add):

    • Fill in a valid user.
    • Click “Add User”. Observe the button text change to “Adding User…” (briefly, as our mock API is fast).
    • Simulate Network Delay/Error: With network throttling enabled, try adding a user. The “Adding User…” text should persist for longer. If you stop the server before clicking “Add User”, you should see the error message in the form.

Mini-Challenge: Add a Global Error Notification (Conceptual)

Our current error handling is localized to the component where the error occurs. In a real application, you often want a more global, non-intrusive error notification (e.g., a toast message or a snackbar) for critical failures.

  1. How could you modify UserService to also notify a global ErrorService whenever handleError is called?
  2. Imagine a GlobalNotificationComponent in AppComponent that subscribes to this ErrorService and displays a toast. What would be the high-level steps to implement this?

(This is a conceptual challenge to think about global state management for errors, not requiring code implementation.)

Summary/Key Takeaways

  • We’ve ensured robust loading and error feedback for users, crucial for a good user experience.
  • The UserService effectively manages loadingUsers and errorLoadingUsers states using BehaviorSubjects.
  • UserListComponent and UserFormComponent accurately map these service states to local signals, which then drive conditional rendering in their templates using @if.
  • The computed signal in UserFormComponent demonstrates a clean way to derive UI state (like isAddingUser) from service states.
  • We’ve used browser developer tools to simulate network conditions and thoroughly test our loading and error states.

Our application is becoming more user-friendly and resilient. In the final project chapter, we’ll wrap things up by writing unit tests for our core components and services using Vitest, confirming their functionality.