DEV Community

Cover image for Ng-News 25/44: Resource Composition & Community Content
ng-news
ng-news

Posted on

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

A new PR introduces Resource Composition — a major step toward creating derived resources cleanly and safely. Plus: new Signal Forms docs, rxResource insights, zoneless error handling, and performance talk.

🧩 Resource Composition

An interesting PR popped up that introduces a feature called Resource Composition.

If we have a resource and want to create a derived resource, we can technically do this via a computed.

Given the following resource:

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

A resource which is based on user, could be done via:

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

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

Unfortunately projectedUser does not satisfy the type of Resource<ProjectedUser | undefined>.

It would also become fragile in the future — if the Resource type is ever extended with new methods or properties, every custom implementation would have to be updated accordingly.

A potential alternative could be to perform the mapping directly inside the original resource, for example within its loader, stream, or parse function.

But that would be a misuse of their intended purpose and would also mean losing access to the original value.

If we look more closely at a readonly resource, we can identify its core state as consisting of three properties:

  • value,
  • status, and
  • error.

Everything else can be derived from these three. These three elements form the ResourceSnapshot.

Therefore, when we want to produce a derived value, all we need is the snapshot — and our mapping function can both take and return a snapshot.

Reconstructing the full readonly resource from that is straightforward, since the framework already provides the necessary helper function:

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

From a typing perspective, ResourceSnapshot is also preferable: it’s a union type, which enables more precise type narrowing across the different states.

An obvious question might be: Why not replace the existing Resource entirely with ResourceSnapshot?

That could indeed work for readonly resources (Resource<T>), but not for ResourceRef<T>, which includes the loading and streaming logic.

ResourceSnapshot represents only the state, while ResourceRef provides the reactive lifecycle on top of it.

feat(core): resource composition via snapshots #64811

  • Define ResourceSnapshot<T> as a type union of possible states for a Resource<T>.
  • Add Resource.snapshot() to convert a Resource to a signal of its snapshot.
  • Add resourceFromSnapshots to convert a reactive snapshot back into a Resource.

By converting resources from/to Signal<ResourceSnapshot>s, full composition of resources is now possible on top of signal composition APIs like computed and linkedSignal.

For example, a common feature request is to have a Resource which retains its value when its reactive source (params) changes. This can now be built as a utility, leveraging linkedSignal's previous value capability:

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

This feature has already been merged. We'll see if it makes into Angular 21. Resource are still experimental.

📖 Signal Forms Documentation

For the upcoming Signal Forms, which will also start experimental, the official documentation has now landed.
You are invited to give it a first look and share your feedback with the Angular team.

Forms with signals • Angular

The web development framework for building modern apps.

favicon next.angular.dev

🔍 rxResource Design Decisions

Johannes Hoppe, from Angular Schule, published an article discussing three design decisions about that rxResource that may be unintuitive at first sight or could be done better.


When rxResource fetches new data, it doesn’t keep the previous value until the response for the new request arrives.

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

When we reload a resource, a potential error also doesn’t go away until the new response is in.

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

And there's an issue when using rxResource with HttpClient, where the HTTP error is wrapped into another error.

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

He titled his article "rxResource is broken", which can sound misleading - but it’s really about design decisions that the Angular team took on purpose and that may evolve over time.
In fact, the upcoming snapshot feature is already the solution for the value issue, when the resource loads.

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

🚀 Angular ships with three Resource APIs for declarative async data loading: resource(), rxResource(), and httpResource(). They&#39;re powerful additions to Angular&#39;s reactive toolkit, but they share a common foundation with some sharp edges. This article examines three bugs in the shared core, supports them with source code, and shows how to fix each one.

favicon angular.schule

🚨 Error Handling in Zoneless Apps

Ricky Lopes discussed how error handling changes in zoneless applications.

With zone.js, asynchronous errors were automatically caught and passed to the registered ErrorHandler.

Without zone.js, we need to call provideBrowserGlobalErrorListeners() to get that behavior back.

If you create a new Angular application today, this is already included by default.

⚡️ Backend Performance Matters

Mario Budischek looked at performance optimization in Angular, arguing that most bottlenecks start on the backend — where slow responses just block the frontend.

So when optimizing your application, focus on backend performance first.

Top comments (0)