Introducing @void-snippets/angular: Signal-Native CRUD for Angular 19+
Type-safe, batteries-included data fetching for Angular with MaybeSignal reactivity and full optimistic stack support.
Every Angular app rewrites the same data-fetching layer: fetch lists, track loading states, handle errors, invalidate caches, mutate with optimistic updates. The code isn't hard—it's just identical every time.
@void-snippets/angular extracts that boilerplate once, built ground-up for Angular 19+ and Signals. No async pipes. No subscription management. Just inject() and reactive Signals that automatically respond to your data.
The Problem We're Solving
Building a contacts CRUD feature typically looks like:
// The old way — lots of manual stateexport class ContactsComponent { private apiService = inject(ContactsApiService); private queryClient = inject(QueryClient);
// ... mutations, pagination, refetch logic all manual
}
Manual state management.Observable subscriptions.Error handling scattered everywhere.Cache invalidation forgotten.
The void-snippets Way
import { inject } from '@angular/core';import { createResourceInject } from '@void-snippets/angular';import { ContactsService } from './contacts.service';import type { Contact } from './contacts.types';export function useContactResource() { return createResourceInject('contacts', inject(ContactsService), { defaultParams: { page: 1, limit: 20 }, optimistic: { update: (cache, { _id, payload }) => cache.map(c => c._id === _id ? { ...c, ...payload } : c), remove: (cache, id) => cache.filter(c => c._id !== id), }, });}// In your component:export class ContactsComponent { private resource = useContactResource(); // ✨ That's it. Everything you need: contacts = this.resource.injectList(); mutations = this.resource.injectMutations();}
One injection. All the state. Auto-managed cache. Optimistic updates baked in.
The Angular Innovation: MaybeSignal
Reactive parameters are where void-snippets/angular shines:
// Static parameters — fetches onceconst contacts = resource.injectList({ page: 1, limit: 20 });// Reactive parameters — auto-refetches when signal changesconst params = signal({ page: 1, limit: 20 });const contacts = resource.injectList(params);// Update the signal, query fires automatically:params.set({ page: 2, limit: 20 });// contacts.list() updates with new data
No extra wiring. No effect() subscriptions. No switchMap() operators. Pass a Signal as a parameter, and the query just reacts. That's MaybeSignal<T> — accepts plain objects or Signals, same code either way.
This works because the inject factory reads your Signal inside TanStack Angular Query's reactivity boundary. Update the Signal → dependency changes → query refires.
Full Feature Parity with React
What you get from createResourceInject:
Feature
Status
injectList() — paginated queries with 3 loading states
✅ Signal-based
injectGet(id) — single-item queries, auto-disable on empty ID
✅ Signal-based
injectMutations() — typed create/update/remove with Signal states
✅ Signal-based
injectInfinite() — infinite scroll with flattened allItems
✅ Signal-based
Optimistic updates with rollback
✅ WeakMap stack
Custom adapters
✅ Same API as React
Error handling and retry
✅ Via TanStack Query
Same battle-tested optimistic stack from @void-snippets/react. Same cache semantics. Different API, optimized for Angular.
// contacts-list.component.tsimport { Component, signal, computed } from '@angular/core';import { CommonModule } from '@angular/common';import { MatTableModule } from '@angular/material/table';import { useContactResource } from './contacts.resource';import type { Contact } from './contacts.types';@Component({ selector: 'app-contacts-list', standalone: true, imports: [CommonModule, MatTableModule], template: ` @if (contacts.isLoading()) { <div>Loading contacts...</div> } @else if (contacts.isError()) { <div>Error: {{ contacts.error()?.message }}</div> <button (click)="contacts.refetch()">Retry</button> } @else { <table mat-table [dataSource]="contacts.list()"> <!-- columns and rows --> </table> } `,})export class ContactsListComponent { private resource = useContactResource(); contacts = this.resource.injectList(); mutations = this.resource.injectMutations(); async removeContact(id: Contact.Id) { // Optimistic update happens instantly // Rollback on error is automatic await this.mutations.remove.mutateAsync(id); }}
That's it. Pagination, loading states, errors, mutations, optimistic updates, all managed. No manual subscription handling. No async pipes. Pure Signal reactivity.
Core is dependency-free — shared types, adapters, pagination interfaces. Angular and React are independent — each optimized for its framework. No forced compatibility. No pulling in React's peer deps into Angular apps.
Angular 19+ apps that are tired of writing the same list/detail/mutations code five times should give this a shot. Type safety. Zero boilerplate. Signals all the way down.
Questions? Feedback? Open an issue or hit me up on LinkedIn or GitHub.