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:
isAddingUserComputed Signal: This computed signal directly reflects theuserService.loadingUsers.getValue()state. This provides a clean way to react to the service’s loading status.- Submit Button Text: We use an
@ifblock directly in the button content to show “Adding User…” whenisAddingUser()is true, providing instant feedback. - Submit Button
disabledAttribute: The button is disabled ifuserForm.invalid()OR ifisAddingUser()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. Restartjson-serverand 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.
- How could you modify
UserServiceto also notify a globalErrorServicewheneverhandleErroris called? - Imagine a
GlobalNotificationComponentinAppComponentthat subscribes to thisErrorServiceand 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
UserServiceeffectively managesloadingUsersanderrorLoadingUsersstates usingBehaviorSubjects. UserListComponentandUserFormComponentaccurately map these service states to local signals, which then drive conditional rendering in their templates using@if.- The
computedsignal inUserFormComponentdemonstrates a clean way to derive UI state (likeisAddingUser) 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.