In the evolving landscape of Angular state management, Angular Signals provide a simpler and more intuitive way to manage state compared to traditional solutions like Observables and RxJS. In this enhanced post, we'll explore how you can convert Angular Signals to a state store, illustrating the concept with a practical example that integrates Signals with services and Observables.
We'll walk through how to manage blog posts as a state using Signals and make them available via Observables for consumption in a component. Additionally, we’ll enhance the functionality for better performance and code clarity.
The Basics: Converting Signals to a State Store
Let’s start by converting Angular Signals into a state store that can hold and manage an array of blog posts. The key here is to use Signals for the reactive state and Observable to expose the state to components that are used to the RxJS pattern.
Below is an example using Angular’s inject() function to get the service and manage the state store using a signal.
Initial Example:
import { inject, signal, WritableSignal, toObservable } from '@angular/core';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { BlogPostDto, DefaultService } from './services/default.service'; // Assuming DTO and service are imported
export class BlogService {
private blogService: DefaultService = inject(DefaultService);
// Step 1: Declare the WritableSignal as a state store for BlogPostDto[]
private blogStores: WritableSignal = signal([]);
// Step 2: Expose the Signal as an Observable for external use
blogs$: Observable;
constructor() {
// Convert the signal to an observable
this.blogs$ = toObservable(this.blogStores.asReadonly());
// Do NOT call fetchBlogs() here; it will be called explicitly later
}
// Step 3: Create a method to fetch a specific blog by its slug
blogBySlug$ = (slug: string) => this.blogs$.pipe(
map(blogs => blogs.find((blog) => blog.slug === slug))
);
// Fetch blogs lazily when explicitly requested
fetchBlogs(): void {
this.blogService.getBlogs().subscribe({
next: (blogs: BlogPostDto[]) => {
this.blogStores.set(blogs);
}
});
}
}
Enhancing the Code:
We can make a few improvements to enhance the code for readability, performance, and reactivity.
1. Lazy Loading of Blogs:
Instead of fetching blogs immediately in the constructor, we explicitly call fetchBlogs() only when needed. This prevents unnecessary API calls if the data is not used immediately.
2. Error Handling:
It's essential to include error handling in the getBlogs() subscription to avoid silent failures when the API fails.
3. Reactive Blog Search:
Instead of piping the slug search through map, we can use Signals more effectively by creating a reactive search directly on the store, which updates whenever new blog data is fetched.
Enhanced Example:
import { inject, signal, WritableSignal, toObservable, computed } from '@angular/core';
import { Observable, of } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
import { BlogPostDto, DefaultService } from './services/default.service';
export class BlogService {
private blogService: DefaultService = inject(DefaultService);
// Declare the state signal for blogs
private blogStores: WritableSignal = signal([]);
// Expose the signal as an Observable
blogs$: Observable;
constructor() {
// Convert the signal into an observable to expose externally
this.blogs$ = toObservable(this.blogStores.asReadonly());
}
// Method to fetch blogs with error handling
fetchBlogs(): void {
this.blogService.getBlogs()
.pipe(
catchError(error => {
console.error('Error fetching blogs:', error);
return of([]); // Return an empty array on error
})
)
.subscribe({
next: (blogs: BlogPostDto[]) => {
this.blogStores.set(blogs); // Update state with new blogs
}
});
}
// Reactive search using a signal for finding a blog by slug
blogBySlug(slug: string): BlogPostDto | undefined {
const blogSignal = computed(() => this.blogStores().find(blog => blog.slug === slug));
return blogSignal(); // Immediately return the blog if found, or undefined if not
}
}
Explaining the Key Changes:
1. Lazy Blog Fetching:
In this version, fetchBlogs() is called explicitly when needed, instead of triggering it in the constructor. This makes the service more efficient.
this.fetchBlogs(); // Called explicitly, not in the constructor
2. Error Handling:
We’ve added a catchError operator to handle API errors gracefully by logging them and returning an empty array to prevent the app from breaking.
.pipe(catchError(error => {
console.error('Error fetching blogs:', error);
return of([]); // Fallback in case of error
}))
3. Reactive Blog Search:
By using the computed() function from Signals, we create a reactive signal that directly tracks the dependency on the slug. This way, whenever the blog list is updated, the signal for the specific blog is automatically recalculated.
const blogSignal = computed(() => this.blogStores().find(blog => blog.slug === slug));
return blogSignal();
Use Case: Exposing Blogs to Components
Now that we have our state store set up, let’s see how we can utilize it in an Angular component.
Blog List Component:
@Component({
selector: 'app-blog-list',
template: `
<div *ngFor="let blog of blogs$ | async">
<h2>{{ blog.title }}</h2>
<p>{{ blog.excerpt }}</p>
</div>
`
})
export class BlogListComponent implements OnInit {
blogs$: Observable;
constructor(private blogService: BlogService) {}
ngOnInit(): void {
// Explicitly call fetchBlogs when the component is initialized
this.blogService.fetchBlogs();
// Subscribe to the blog state store
this.blogs$ = this.blogService.blogs$;
}
}
Blog Detail Component:
@Component({
selector: 'app-blog-detail',
template: `
<h1>{{ blog?.title }}</h1>
<p>{{ blog?.content }}</p>
`
})
export class BlogDetailComponent implements OnInit {
blog: BlogPostDto | undefined;
constructor(private blogService: BlogService, private route: ActivatedRoute) {}
ngOnInit(): void {
const slug = this.route.snapshot.paramMap.get('slug')!;
this.blog = this.blogService.blogBySlug(slug);
}
}
Conclusion
By converting Angular Signals into a state store, we gain the benefits of both fine-grained reactivity and compatibility with RxJS-based Observable streams. This approach leads to more efficient state management, fewer unnecessary updates, and a more readable codebase. In the enhanced example, we’ve also introduced lazy loading, error handling, and reactive signals to optimize performance and user experience.
Angular Signals are a promising addition to Angular's reactive capabilities, and they offer a simpler, yet powerful alternative to more complex RxJS patterns. Experiment with them in your Angular projects and explore how they can streamline your state management solutions!
