Angular NgRx Signal Store Resource: State Management Revolution

The Angular Resource API is an exciting new tool that simplifies state management in modern web applications. Paired with NgRx Signal Store, this powerful combination promises to revolutionize the way developers manage state, fetch data, and enhance performance in Angular applications. The integration of these tools provides businesses with a competitive advantage, streamlining workflows, reducing boilerplate code, and improving application performance.

In this article, we’ll walk through how to leverage the Resource API along with NgRx Signal Store to build a state management solution that is both declarative and powerful, with minimal boilerplate and excellent developer ergonomics.

Why the Resource API?

For developers who have been managing state in Angular, the challenges are clear. From dealing with complex services, observables, and multiple action types, to handling data-fetching with external APIs, Angular developers often juggle many complexities. The Resource API is a leap forward by making asynchronous requests and state management simpler and more intuitive.

What Makes This Solution Powerful

  • Declarative State Management: Angular's new Resource API makes it easy to manage remote state and cache results with minimal configuration.
  • Minimal Boilerplate: Less configuration means more productivity. The Resource API and NgRx Signal Store integration reduce the amount of code developers need to write, focusing more on business logic.
  • Type Safety: Angular's modern tooling ensures that developers are working in a type-safe environment, reducing runtime errors.
  • Reactive by Design: The combination of NgRx Signal Store and the Resource API offers a reactive paradigm, aligning perfectly with Angular’s change detection and reactivity model.

Implementation Deep Dive

Now that we understand the power behind the Resource API, let’s see how we can combine it with NgRx Signal Store for an intuitive, scalable state management solution.

Setting Up the Signal Store Feature with Resource API

import {
  signalStoreFeature,
  withMethods,
  type EmptyFeatureResult,
  type SignalStoreFeature,
  type SignalStoreFeatureResult,
  type Prettify,
  type StateSignals,
  type WritableStateSource,
} from '@ngrx/signals';
import {
  ResourceOptions,
  ResourceRef,
  resource,
} from '@angular/core';

const lowerFirstLetter = <T extends string>(value: T): Uncapitalize<T> => {
  if (typeof value !== 'string') {
    return value;
  }

  const trimmed = value.trim();
  const first: string = trimmed.charAt(0).toLowerCase();
  const rest: string = trimmed.slice(1);

  return `${first}${rest}` as Uncapitalize<T>;
};

export type ResourceStore<Input extends SignalStoreFeatureResult> = Prettify<
  StateSignals<Input['state']> & Input['computed'] & Input['methods'] & WritableStateSource<Prettify<Input['state']>>
>;

export type CreateResourceFn<
  TDataFn = unknown,
  Input extends SignalStoreFeatureResult = SignalStoreFeatureResult,
> = (store: ResourceStore<Input>) => () => ResourceOptions<TDataFn, unknown>;

export type ResourceProp<Name extends string> = `${Uncapitalize<Name>}Resource`;

export type ResourceMethod<TData = unknown> = (() => ResourceRef<TData>) &
  ResourceRef<TData>;

export const withResourceFeature = <
  Name extends string,
  TDataFn = unknown,
  TData = TDataFn,
  Input extends SignalStoreFeatureResult = SignalStoreFeatureResult,
>(
  name: Name,
  createResourceFn: CreateResourceFn<TData, NoInfer<Input>>,
): SignalStoreFeature<
  Input,
  EmptyFeatureResult & { methods: Record<ResourceProp<NoInfer<Name>>, ResourceMethod<NoInfer<TData>>> }
> => {

  return signalStoreFeature(
    withMethods((store) => {

      const resourceOptions = createResourceFn(store as ResourceStore<NoInfer<Input>>)
      const resourceTarget = resource(resourceOptions());

      const prop: ResourceProp<NoInfer<Name>> = `${lowerFirstLetter(name)}Resource`;

      return {
        [prop]: new Proxy(() => resourceTarget, {
          get: (_, prop) => Reflect.get(resourceTarget, prop),
          has: (_, prop) => Reflect.has(resourceTarget, prop),
        }),
      } as Record<ResourceProp<NoInfer<Name>>, ResourceMethod<NoInfer<TData>>>;
    }),
  )
};

Creating the Store with Resource Feature

Next, let’s create a store with the Resource API to manage remote data:

import { patchState, signalStore, withMethods, withState } from '@ngrx/signals';
import { withResourceFeature } from '../store/with-resource.feature';
import { JsonPipe } from '@angular/common';

const PostStore = signalStore(
  withState({
    postId: 1,
  }),
  withMethods(store => ({
    nextPost : () => {
      patchState(store, (state) => ({
        postId: state.postId > 9 ? 1 : state.postId + 1
      }));
    }
  })),
  withResourceFeature('postId', (store) => {
    return () => {
      return {
        request: () => store.postId(),
        loader: async ({request: postId, abortSignal }) => {
          const API_URL = 'https://jsonplaceholder.typicode.com'

          const response = await fetch(`${API_URL}/posts/${postId}`, {
            signal: abortSignal,
          });

          if(!response.ok) throw new Error(`Error fetching post id: ${postId}`);

          const jsonResponse = await response.json();
          return jsonResponse as {
            userId: number;
            id: number;
            title: string;
            body: string;
          };
        }
      };
    };
  })
);

Component Integration

Now, let’s wire everything up into a component where we display the fetched data:

@Component({
  selector: 'app-post-details',
  standalone: true,
  providers: [PostStore],
  imports: [JsonPipe],
  template: `
    @let post = store.postIdResource();

    @if(post.isLoading()) {
      <div>Loading...</div>
    }
    @if(post.error()) {
      <div>Error</div>
    }
    @if(post.value()) {
      <pre>{{ post.value() | json }}</pre>
    }

    <button (click)="store.nextPost()">Fetch Next Post</button>
    <button (click)="post.reload()">Refetch Post</button>
  `,
})
export class ProductDetailsComponent {
  readonly store = inject(PostStore);
}

Real-World Benefits

  1. Simplified State Logic: Less code, more functionality.
  2. Optimized Performance: Built-in caching and background updates.
  3. Easier Debugging: Clear, predictable state transitions.
  4. Faster Development: Focus on logic, not configuration.

Potential Pitfalls to Watch

  • Always use inject() for service dependencies.
  • Leverage ChangeDetectionStrategy.OnPush religiously.
  • Handle error states gracefully.

Wrapping Up

With the Resource API in Angular, we are not just looking at a new feature—it's a paradigm shift for how we manage state and fetch data. Combining it with NgRx Signal Store enables developers to build more reactive, efficient, and maintainable applications with minimal effort. As Angular continues to innovate, tools like this will help shape the future of web development.

Happy Coding!