A recent pull request adds Resource Composition — a significant move toward deriving resources in a clean, safe manner. Also: updated documentation for Signal Forms, insights into rxResource, error handling without zone.js, and discussions on performance.

🧩 Resource Composition

A noteworthy PR has surfaced that proposes a new capability labeled Resource Composition.

When you have an existing resource and need a secondary one based on it, a computed is a viable approach.

Take this resource as an example:

type User = {
  id: number;
  firstname: string;
  lastname: string;
  email: string;
};

type ProjectedUser = {
  id: number;
  email: string;
  name: {
    first: string;
    last: string;
  };
};

function mapToProjectedUser(user: User | undefined) {
  if (!user) {
    return undefined;
  }

  return {
    id: user.id,
    email: user.email,
    name: {
      first: user.firstname,
      last: user.lastname,
    },
  };
}

const user = resource({
  loader: () =>
    new Promise<User>((resolve) =>
      resolve({ id: 1, firstname: 'John', lastname: 'Doe', 
                email: 'john.doe@example.com' })
    ),
});
Enter fullscreen mode Exit fullscreen mode

When a resource is derived from user, the implementation might look like this:

const userValue = computed(() => mapToProjectedUser(user.value()));

const projectedUser = { ...user, value: userValue }
Enter fullscreen mode Exit fullscreen mode

Here, projectedUser fails to meet the expected type of Resource<ProjectedUser | undefined>.

Looking ahead, this approach would turn brittle—if the Resource type ever gains additional methods or properties, each custom implementation would need to be adapted in turn.

An alternative might be to apply the mapping directly in the original resource, say in its loader, stream, or parse.

However, that would twist those functions out of their intended roles and also sacrifice access to the underlying value.

Examining a readonly resource more closely, its fundamental state boils down to three fields:

  • value,
  • status, and
  • error.

Everything beyond that is computable from these three. Together, they constitute the ResourceSnapshot.

So, to generate a derived value, only the snapshot is required—our mapping function can accept and return a snapshot.

Rebuilding the full readonly resource from it is easy, since the framework ships with a helper for exactly this purpose:

function withProjectedUser(user: Resource<User | undefined>) {
  const projectedUser = computed(() => {
    const snap = user.snapshot();

    if ('value' in snap) {
      return { ...snap, value: mapToProjectedUser(snap.value) };
    }

    return snap;
  });

  return resourceFromSnapshots(projectedUser);
}

const projectedUser = withProjectedUser(user);
Enter fullscreen mode Exit fullscreen mode

When it comes to typing, ResourceSnapshot has another edge: because it is a union type, narrowing between the various states becomes far more straightforward.

You might wonder: why not just swap out Resource for ResourceSnapshot across the board?

For read-only resources (Resource<T>), that substitution would work, but ResourceRef<T> is a different story—it carries the loading and streaming machinery.

ResourceSnapshot is purely a state description, while ResourceRef layers the reactive behavior on top of that state.

GitHub logo feat(core): resource composition via snapshots #64811

alxhub avatar
alxhub posted on
  • Introduce ResourceSnapshot<T>, a union type representing all attainable states of Resource<T>.
  • Expose Resource.snapshot(), which turns a Resource into a signal carrying its snapshot.
  • Provide resourceFromSnapshots to reverse the process—building a Resource from a reactive snapshot.

With the ability to interchange Resource instances and Signal<ResourceSnapshot>s, resources can now be fully composed using the same primitives that drive signal composition, including computed and linkedSignal.

A frequently requested pattern—such as a Resource that keeps its prior value during reactive source (params) updates—can now be implemented as a reusable utility that takes advantage of linkedSignal's capability to preserve previous state:

function withPreviousValue<T>(input: Resource<T>): Resource<T> {
  const derived = linkedSignal({
    source: input.snapshot,
    computation: (snap, previous) => {
      if (snap.status === 'loading' && previous?.value) {
        // When the input resource enters loading state, we keep the value
        // from its previous state, if any.
        return {status: 'loading', value: previous.value.value};
      }

      // Otherwise we simply forward the state of the input resource.
      return snap;
    },
  });

  return resourceFromSnapshots(derived);
}

// In application code:

userId = input.required<number>();
user = withPreviousValue(httpResource(() => `/user/{this.userId()}`));
// if `userId()` switches, `user.value()` will keep the old value until
// the new one is ready!
Enter fullscreen mode Exit fullscreen mode

The merge for this feature is already done. Whether it lands in Angular 21 remains to be seen, and Resource is still in the experimental phase.

📖 Signal Forms Documentation

Signal Forms, set to launch as experimental as well, now has its official documentation in place.
You're encouraged to take an early look and pass your thoughts along to the Angular team.

Ng-News 25/44: Resource Composition & Community Content — figure 3

Forms with signals • Angular

A framework for crafting contemporary web applications.

favicon

🔍 rxResource Design Decisions

Johannes Hoppe, hailing from Angular Schule, has put together an article where he examines three aspects of rxResource that might strike you as puzzling on the first encounter, or perhaps could have been handled differently.


With rxResource, any fresh data fetch does not preserve the former value while waiting for the reply concerning the new request.

it('should loose the value once it fetches new data', async () => {
  const injector = TestBed.inject(Injector);
  const number = signal(0);

  const numResource = rxResource({
    params: number,
    stream: ({ params: value }) => of(value),
    injector,
  });

  await expect.poll(() => numResource.value()).toBe(0);

  number.set(1);

  // 👇 could be an unexpected behavior
  expect(numResource.value()).toBe(undefined);
});
Enter fullscreen mode Exit fullscreen mode

Additionally, when a resource is reloaded, any pending error carries over until the fresh response arrives.

it('should keep the error when reloading', async () => {
  const injector = TestBed.inject(Injector);
  const number = signal(1);

  const numResource = rxResource({
    stream: () => (number() % 2 === 1 ? throwError(() => new Error('Odd number')) : of(number())),
    injector,
  });

  await expect.poll(() => numResource.status()).toBe('error');

  number.set(2);
  numResource.reload();

  // 👇 error should go away when reloading
  expect(numResource.error()).toBeInstanceOf(Error);
});
Enter fullscreen mode Exit fullscreen mode

A problem also surfaces when pairing rxResource with HttpClient: the original HTTP error gets nested inside another error layer.

it('should wrap an HttpError into an Error', async () => {
  const injector = TestBed.inject(Injector);
  const number = signal(0);

  const http = rxResource({
    stream: () => throwError(() => new HttpErrorResponse({ status: 500 })),
    injector,
  });

  await expect.poll(() => http.status()).toBe('error');
  expect(http.error()).toBeInstanceOf(Error);

  // 👇 error should actually be the HttpErrorResponse
  expect(http.error()).not.toBeInstanceOf(HttpErrorResponse);
});
Enter fullscreen mode Exit fullscreen mode

His post is called "rxResource is broken," a heading that might appear off-base at first glance. Yet the real subject is the deliberate design choices made by the Angular team, which could shift in future iterations.
Interestingly, the upcoming snapshot capability already addresses the concern about values while the resource is still loading.

Ng-News 25/44: Resource Composition & Community Content — figure 5

Angular.Schule → Angular's Resource APIs Are Broken - Let's Fix Them!

🚀 Angular provides three Resource APIs—resource(), rxResource(), and httpResource()—for declarative async data fetching. These are strong enhancements to Angular's reactive toolset, yet they rest on a shared foundation that has a few rough spots. The post digs into three defects in that common core, backs them up with code snippets, and offers solutions for each issue.

favicon

🚨 Error Handling in Zoneless Apps

Ricky Lopes took a look at the shifts in error handling for apps running without zones.

Under zone.js, async errors got intercepted automatically and routed to the ErrorHandler that had been registered.

In a zoneless environment, calling provideBrowserGlobalErrorListeners() is required to restore that same behavior.

Today, any freshly scaffolded Angular application is already set up with this enabled.

⚡️ Backend Performance Matters

Mario Budischek explored Angular performance tuning and contended that the root of most slowdowns lies in the backend, since sluggish server responses simply stall the UI.

Thus, when you’re tweaking an app’s speed, tackle backend efficiency before anything else.