Angular HTTP request tracking using signals - standalone and module-free
npm install signals-http-trackingA TypeScript-first, reactive HTTP state management library built on Angular Signals. Provides clean APIs for single and multiple HTTP requests with built-in loading states, error handling, and debouncing.
- ๐ Full Type Safety - Complete TypeScript inference and compile-time checks
- โก Reactive Signals - Built on Angular's signal system for optimal performance
- ๐ Automatic State Management - Loading, success, error states handled automatically
- ๐ฏ HTTP Tracking Integration - Works seamlessly with existing global HTTP trackers
- โฑ๏ธ Built-in Debouncing - Request debouncing using RxJS operators
- ๐ ForkJoin Support - Handle multiple parallel HTTP requests effortlessly
- โ๏ธ Chainable API - Fluent method chaining for better developer experience
- ๐งน Automatic Cleanup - Subscription management handled internally
``bash`
npm install @your-org/signal-actions-http-store
`typescript
import { createSignalAction } from '@your-org/signal-actions-http-store';
@Injectable()
export class UserStore {
private userService = inject(UserService);
user = signal
fetchUser = createSignalAction<[string], User>(
(userId: string) => this.userService.getUser(userId),
{
onSuccess: (user) => this.user.set(user),
onError: (error) => console.error('Failed to fetch user:', error)
}
);
}
`
`typescript`
getUserData = createSignalForkJoinAction<
{ userId: string },
{ user: User; posts: Post[] }
>(
(request: { userId: string }) => ({
user: this.userService.getUser(request.userId),
posts: this.postService.getUserPosts(request.userId)
}),
{
onSuccess: (result) => {
this.user.set(result.user);
this.posts.set(result.posts);
}
}
);
Creates a signal-based action for single HTTP requests.
Parameters:
- observableFn: (...args: TArgs) => Observable - Function that returns an Observableoptions?: ActionOptions
- - Configuration options
Returns: SignalAction
Creates a signal-based action for multiple parallel HTTP requests.
Parameters:
- observablesFn: (request: TRequest) => Record - Function that returns an object of Observablesoptions?: ActionOptions<[TRequest], TResult>
- - Configuration options
Returns: SignalAction<[TRequest], TResult>
`typescript`
interface ActionOptions
track?: boolean; // Enable/disable HTTP tracking (default: true)
debounceMs?: number; // Debounce time in milliseconds
onLoading?: (...args: TArgs) => void; // Called when request starts
onSuccess?: (data: TResult, ...args: TArgs) => void; // Called on successful response
onError?: (error: string, ...args: TArgs) => void; // Called on error
}
`typescript`
interface SignalAction
readonly status: WritableSignal
readonly error: WritableSignal
readonly isLoading: Signal
readonly isSuccess: Signal
readonly isError: Signal
readonly run: (...args: TArgs) => ActionHandlers
}
`typescript
searchUsers = createSignalAction<[string], User[]>(
(query: string) => this.userService.searchUsers(query),
{
debounceMs: 300, // Wait 300ms after user stops typing
onSuccess: (users) => this.searchResults.set(users),
onError: (error) => this.snackbar.open(error)
}
);
// In component
onSearchInput(query: string) {
this.store.searchUsers.run(query);
}
`
`typescript`
applyFilters = createSignalForkJoinAction<
{ userFilters: UserFilterRequest; postFilters: PostFilterRequest },
{ users: User[]; posts: Post[] }
>(
(request: { userFilters: UserFilterRequest; postFilters: PostFilterRequest }) => ({
users: this.userService.getFilteredUsers(request.userFilters),
posts: this.postService.getFilteredPosts(request.postFilters)
}),
{
debounceMs: 500, // Wait for user to finish adjusting filters
onSuccess: (result) => {
this.filteredUsers.set(result.users);
this.filteredPosts.set(result.posts);
}
}
);
`typescript
@if (store.fetchUser.isLoading()) {
} @else if (store.fetchUser.isError()) {
{{ store.fetchUser.error() }}
} @else if (store.fetchUser.isSuccess()) {
User loaded successfully!
}
@if (store.user(); as user) {
๐ Chainable API
Actions return handlers that can be chained for component-specific logic:
`typescript
this.store.fetchUser.run('123')
.onLoading((userId) => console.log('Loading user:', userId))
.onSuccess((user, userId) => {
console.log('User loaded:', user);
this.analytics.track('user_loaded', { userId });
})
.onError((error, userId) => {
console.error('Failed to load user:', error);
this.analytics.track('user_load_failed', { userId, error });
})
.finally(() => {
console.log('User fetch attempt completed');
});
`โ ๏ธ Important Notes
$3
With
createSignalForkJoinAction, if any HTTP request fails, the entire operation fails:`typescript
// โ If posts request fails, you get NO data (even if user request succeeded)
getUserData.run(request)
.onSuccess(({ user, posts }) => {
// Only called if BOTH requests succeed
})
.onError((error) => {
// Called if ANY request fails
});
`$3
If you need partial results, handle errors within individual observables:
`typescript
getUserData = createSignalForkJoinAction(
(request: BasicQuery) => ({
user: this.userService.getUser(request).pipe(
catchError(() => of(null)) // Return null instead of failing
),
posts: this.postService.getPosts(request).pipe(
catchError(() => of([])) // Return empty array instead of failing
)
}),
{
onSuccess: (result) => {
if (result.user) this.user.set(result.user);
if (result.posts.length) this.posts.set(result.posts);
}
}
);
`๐ Best Practices
$3
- Use store-level
onSuccess/onError for state updates
- Use component-level .onSuccess() for navigation/UI logic
- Add debouncing for user input (search, filters, auto-save)
- Use descriptive generic types: createSignalAction<[UserId], User>
- Keep HTTP logic in services, state management in stores$3
- Mix store state updates in components
- Forget that forkJoin is "all or nothing"
- Add
take(1) to HTTP observables (they auto-complete)
- Use without TypeScript generics (you'll lose type safety)
- Create actions inside components (create them in stores/services)๐ง Configuration
$3
The library integrates with your existing
GlobalHttpTracker:`typescript
// HTTP tracking is enabled by default
fetchUser = createSignalAction(getUserFn, {
track: false // Disable tracking for this specific action
});
`$3
`typescript
// Different debounce times for different use cases
searchAction = createSignalAction(searchFn, { debounceMs: 300 }); // Search
filterAction = createSignalAction(filterFn, { debounceMs: 500 }); // Filters
autoSaveAction = createSignalAction(saveFn, { debounceMs: 1000 }); // Auto-save
``