Reactive Flow

To illustrate how the Resource API fits inside a Signal Store, this walkthrough relies on the dessert application familiar from earlier posts in this series:

Example Application

The two filters — Original Name and English Name — kick off the reactive chain, which ultimately renders the desserts with their associated ratings. A button on the page also instructs the store to fetch ratings for the currently visible desserts.

Before diving into code, it helps to sketch this workflow as a reactive graph:

Using Angular’s Resource API with the NGRX Signal Store — figure 2

As the diagram shows, the store accepts the consumer's intent through the three methods on the left. The filter signal feeds the dessertsResource: whenever the filter changes, the resource reloads the dessert list.

Meanwhile, the ratingsResource is activated directly by the loadRatings method, and updateRatings writes into that resource's local working copy. A separate method (omitted here for brevity) could later persist that working copy back to the server.

Consumer's Perspective

Let's first inspect how a consuming component sees this store:

@Component([...])
export class DessertsComponent {
  #store = inject(DessertStore);

  originalName = linkedSignal(() => this.#store.filter.originalName());
  englishName = linkedSignal(() => this.#store.filter.englishName());

  ratedDesserts = this.#store.ratedDesserts;
  loading = this.#store.loading;

  #linkedFilter = computed(() => ({
    originalName: this.originalName(),
    englishName: this.englishName()
  }));

  constructor() {
    this.#store.updateFilter(this.#linkedFilter)
  }

  loadRatings(): void {
    this.#store.loadRatings();
  }

  updateRating(id: number, rating: number): void {
    this.#store.updateRating(id, rating);
  }
}

Since the example ties the originalName and englishName filters to a template-driven form, the component wraps those fields in linked signals. That setup allows two-way binding through ngModel:

<input [(ngModel)]="originalName" name="originalName" />
<input [(ngModel)]="englishName" name="englishName" />

With this binding, any edit made in the form updates the linked signal's local working copy. To propagate those changes upward, the constructor hands the store's updateFilter method a signal representing that working copy.

Note that updateFilter isn't given the raw filter value — it receives a computed linkedFilter signal. As we'll see shortly, this method belongs to the new signalMethod family, which re-executes whenever its input signal changes. In this way, the store's filter state always mirrors the form.

Granted, a reactive form would make this synchronization a bit more straightforward.

The Store

Turning to the store's internal structure, the state is fairly minimal — it only holds the filter:

export type Requested = undefined | true;

export const DessertStore = signalStore(
  { providedIn: 'root' },
  withState({
    filter: {
      originalName: '',
      englishName: 'Cake',
    },
    ratingsRequested: undefined as Requested
  }),
 [...],
);

There are no state fields for desserts or ratings. Those are provided by the resources discussed below, and their loading states can be derived from those same resources. Since ratings loading has no parameters to react to, a simple boolean flag ratingsRequested suffices.

Getting Services via withProps

The store next pulls in withProps to inject the services that the resources and methods will need:

export const DessertStore = signalStore(
  [...],
  withProps(() => ({
    _dessertService: inject(DessertService),
    _ratingService: inject(RatingService),
    _toastService: inject(ToastService),
  })),
  [...],
);

This keeps the store implementation tidy. In earlier versions of the Signal Store, such services had to be threaded through each feature's parameters — a tedious exercise that meant repeating the same injection for every feature that needed it. Centralizing these dependencies via withProps is a cleaner approach.

Also note the leading underscore in these property names. By convention, the Signal Store treats those as private, keeping them out of the consumer's view.

Setting up Resources

The resources themselves are configured in a second withProps block:

export const DessertStore = signalStore(
  [...],
  withProps((store) => ({
    _dessertsResource: resource({
      params: store.filter,
      loader: (loaderParams) => {
        const filter = loaderParams.params;
        const abortSignal = params.abortSignal;
        return store._dessertService.findPromise(filter, abortSignal);
      },
    }),
    _ratingsResource: resource({
      params: store.ratingsRequested
      loader: (loaderParams) => {
        const abortSignal = loaderParams.abortSignal;
        return store._ratingService.loadExpertRatingsPromise(abortSignal);
      }
    })
  })),
  [...],
);

Because this block appears after the one with the services, the resources can reference those injected dependencies. The _dessertsResource wires the store's filter state directly into its params, so any filter change automatically triggers a reload.

More on this: Angular Architecture Workshop (online, interactive, advanced)

Become an expert for enterprise-scale and maintainable Angular applications with our Angular Architecture workshop!Using Angular’s Resource API with the NGRX Signal Store — figure 3

All Details (English Version) | All Details (German Version)

Optional: Exposing Read-Only Resources

Just like the injected services, the resources above are marked private. In this example, they're implementation details — the consumer has no idea whether data arrives via the Resource API, RxJS, or some other mechanism.

If exposing resources to the outside world ever becomes desirable, an additional withProps section can surface read-only versions:

export const DessertStore = signalStore(
  [...],
  withProps((store) => ({
    dessertsResource: store._dessertsResource.asReadonly(),
    ratingsResource: store._ratingsResource.asReadonly(),
  })),
  [...],
);

A read-only resource still exposes the data, loading state, and error info, but it blocks updates to the local working copy. The pattern of keeping resources private and publishing read-only wrappers could be wrapped up in a reusable withResource feature.

Deriving View Models

To prepare data for binding, the store defines a set of computed signals:

export const DessertStore = signalStore(
  [...],
  withComputed((store) => ({
    ratedDesserts: computed(() => toRated(
      store._dessertsResource.value(),
      store._ratingsResource.value()
    )),
    loading: computed(() =>
      store._dessertsResource.isLoading()
        || store._dessertsResource.isLoading())
  })),
  [...]
);

The helper toRated (omitted for brevity) merges desserts with their ratings. The loading signal returns true whenever either resource is still fetching.

Providing Methods

Thanks to the Resource API's declarative approach, the methods themselves carry very little imperative logic:

export const DessertStore = signalStore(
  [...],
  withMethods((store) => ({
    updateFilter: signalMethod<DessertFilter>((filter) => {
      patchState(store, { filter });
    }),
    loadRatings: () => {
        patchState(store, { ratingsRequested: true });
        store._ratingsResource.reload();
    },
    updateRating: (id: number, rating: number) => {
      store._ratingsResource.update(ratings => ({
        ...ratings,
        [id]: rating,
      }));
    },
  })),
  [...],
);

These methods mostly write incoming values into the store's state. The updateFilter method uses the new signalMethod helper. Because it's typed as signalMethod<DessertFilter>, it accepts either a concrete DessertFilter or a signal that emits one. In the latter case, the method fires on every change of that signal.

While the incoming signal is actively tracked, the method's implementation itself is untracked (!) by design. As a result, it won't re-run merely because some signal accessed inside of it changed. That behavior is usually what you want, since it makes the method's execution schedule obvious at a glance.

This helper parallels rxMethod, which shipped with the original Signal Store. Unlike rxMethod, however, signalMethod has no RxJS dependency. That also means race-condition prevention falls on us. In the current example, the Resource API handles that concern.

The loadRatings method flips ratingsRequested to true and then invokes reload on the ratings resource — a pattern covered in more detail elsewhere. Meanwhile, updateRating writes directly into the resource's local working copy. That feels slightly unusual, since stores normally avoid directly mutating writable signals. But it fits the philosophy behind linked signals and resources: temporary data lives inside the reactive flow and gets overwritten as needed. Perhaps a more elegant pattern will emerge with time.

Error Handling with Hooks and Effects

When a request fails, the resource communicates the problem through its error signal. To turn that into a toast notification, the store wires up an onInit hook containing an effect that watches that error signal:

export const DessertStore = signalStore(
 [...],
  withHooks({
    onInit(store) {
      const toastService = store._toastService;
      const dessertsError = store._dessertsResource.error;

      effect(() => {
        const error = store._dessertsResource.error;
        if (error) {
          store._toastService.show('Error: ' + getMessage(error));
        }
      });
    }
  })
);

The getMessage function extracts a user-facing message from the error object:

export function getMessage(error: unknown) {
  if (error && typeof error === "object" && "message" in error) {
    return error.message;
  }
  return String(error);
}

For reusing this behavior across multiple resources, the effect can be extracted into a helper:

export const DessertStore = signalStore(
  [...],
  withHooks({
    onInit(store) {
      const toastService = store._toastService;
      const dessertsError = store._dessertsResource.error;
      const ratingsError = store._ratingsResource.error;

      displayErrorEffect(dessertsError, toastService);
      displayErrorEffect(ratingsError, toastService);
    }
  })
);

That helper takes the errorSignal and the toastService, then installs the same effect:

export function displayErrorEffect(
  errorSignal: Signal<unknown>,
  toastService: ToastService
) {
  effect(() => {
    const error = errorSignal();
    if (error) {
      toastService.show("Error: " + getMessage(error));
    }
  });
}

Everything Together

Now that every piece of a Resource-backed Signal Store has been examined, here's the complete implementation:

export const DessertStore = signalStore(
  { providedIn: "root" },
  withState({
    filter: {
      originalName: "",
      englishName: "Cake",
    },
  }),
  withProps(() => ({
    _dessertService: inject(DessertService),
    _ratingService: inject(RatingService),
    _toastService: inject(ToastService),
  })),
  withProps((store) => ({
    _dessertsResource: resource({
      request: Store.filter,
      loader: (params) => {
        const filter = params.request;
        const abortSignal = params.abortSignal;
        return store._dessertService.findPromise(filter, abortSignal);
      },
    }),
    _ratingsResource: resource({
      loader: (params) => {
        const abortSignal = params.abortSignal;
        return store._ratingService.loadExpertRatingsPromise(abortSignal);
      },
    }),
  })),
  withProps((store) => ({
    dessertsResource: store._dessertsResource.asReadonly(),
    ratingsResource: store._ratingsResource.asReadonly(),
  })),
  withComputed((store) => ({
    ratedDesserts: computed(() =>
      toRated(store._dessertsResource.value(), store._ratingsResource.value())
    ),
    loading: computed(
      () =>
        store._dessertsResource.isLoading() ||
        store._dessertsResource.isLoading()
    ),
  })),
  withMethods((store) => ({
    updateFilter: signalMethod<DessertFilter>((filter) => {
      patchState(store, { filter });
    }),
    loadRatings: () => {
      store._ratingsResource.reload();
    },
    updateRating: (id: number, rating: number) => {
      store._ratingsResource.update((ratings) => ({
        ...ratings,
        [id]: rating,
      }));
    },
  })),
  withHooks({
    onInit(store) {
      const toastService = store._toastService;
      const dessertsError = store._dessertsResource.error;
      const ratingsError = store._ratingsResource.error;

      displayErrorEffect(dessertsError, toastService);
      displayErrorEffect(ratingsError, toastService);
    },
  })
);

Discussion and Outlook

With withProps landing in NgRx 19, a Signal Store can now define arbitrary properties. That simplifies service injection and opens the door to resources. Stores can treat those resources as internal mechanisms, or selectively expose them — ideally as read-only — when consumers need direct access.

Embedding the Resource API directly in a Signal Store makes it possible to build a reactive dataflow entirely with Angular's signal primitives. The signalMethod helper offers a natural bridge between signals and store methods.

Because a resource keeps its own local working copy, user edits can be written into that copy before being persisted. Updating that working copy directly does feel a bit unconventional, as stores usually shy away from mutating writable signals. Yet that's precisely the philosophy behind such working copies: temporary updates happen inside the reactive graph. It's likely the community will eventually land on a more polished pattern. A withResource feature that encapsulates a private resource plus its read-only facade could also prove valuable.