- 
                Notifications
    
You must be signed in to change notification settings  - Fork 44
 
Description
Add a new feature called withEntityResources that integrates withEntities directly into the resource handling pattern.
Background
The ngrx-toolkit currently supports withResources to handle asynchronous read operations. In addition, withEntities (@ngrx/signals/entities) provides a structured way to manage collections of entities.
Many use cases involve resources that represent entity collections. Currently, developers need to manually wire a resource result into an entity state, which adds boilerplate and redundancy. We propose introducing withEntityResource to cover this scenario.
The Challenge: State Structure Mismatch
The main challenge we face is a fundamental mismatch between how resources and entities manage state:
withResourceexpects a state property calledvalueto store the resource resultwithEntitiesprovidesentityMapandidsproperties for entity management
Therefore, both would require manual synchronization.
API Examples
Basic usage
export interface Todo {
  id: number;
  title: string;
  completed: boolean;
}
export const TodoStore = signalStore(
  withEntityResource((store) =>
    httpResource(() => '/todo', {
      defaultValue: [],
      parse: toTodos
    }),
  ),
  withMutations((store) => ({
    addTodo: httpMutation({
      request: (todo: Todo) => ({
        url: 'todo',
        method: 'POST',
        body: todo,
      }),
      parse: toTodos,
      onSuccess((todo: Todo) => {
        patchState(store, addEntity(todo))
      })
    }),
    // ... other mutations
  }))
);
const todoStore = inject(TodoStore);
todoStore satisfies Resource<Todo[]>; // same behavior as in `withResource`
todoStore.value satisfies Signal<Todo[]>;
todoStore.entities satisfies Signal<Todo[]>;
todoStore.addTodo({ id: 2, title: 'Car Wash', completed: false });In this version, withEntityResource works with just one entity. The type of the entity is inferred from the httpResource.
In case the httpResource returns ResourceRef<Todo[] | undefined>, the undefined is ignored.
Named entities
signalStore(
  withEntityResources(({ activeId }) => ({
    todos: httpResource(() => '/todo', {
      defaultValue: [],
      parse: toTodos
    }),
    projects: httpResponse(() => `/project}`), {
        parse: toProjects
    }
  })),
);In case withEntityResources gets passed a dictionary, the keys are used for the collection name and there will be no default entity, i.e. entities property.
Same goes the resources. They are also generated as named resources like in withResource.
Design Overview
- Composed Internally: 
withEntityResourcecallswithEntitiesinternally. - Supports Named Entities: Fully supports named entities, just like 
withEntitiesandwithResource. - Linked Signals for Sync: The internal 
entityMapandidsare created as linkedSignals based on the resource’svalue. This ensures that whenever the resource reloads or updates, the entities are synchronously updated without additional patching logic. - Computed 
value(Read-only): UnlikewithResource, thevalueexposed bywithEntityResourceis not writable and implemented as a computed signal that reflects theentities(NOTvalueof the resource). The reason is that users will be able to modify the entities state but not the resource. Thisvalueis not stored in the state and cannot be patched or manually updated. Instead, the only writable source of truth remains the internal resource, which handles updates exclusively. This avoids conflicts where one part of the code writes tovalueand another toentityMaporids, potentially causing unintentional overrides. - Type-Safe Contract: The 
valuereturned by the resource and the expected entity shape are type-checked for compatibility by design. - Secondary Entry Point: To avoid unnecessary coupling, 
withEntityResourcewill be published as a secondary entry point due to its dependency onwithEntities. 
Alternatives Considered
It could be possible that we provide a "glueing feature", which would glue together an existing resource with an entity. For example:
export const TodoStore = signalStore(
  withEntities<Todo>(),
  withEntityResources((store) =>
    httpResource(() => '/todo', {
      defaultValue: [],
      parse: todo,
    }),
  ),
  withEntityResource(),
);In this case withEntityResource would internally check for an existing resource and entities, verify that they are of the same type and internally replace the value in the state with a computed value and put it into props.
Since this is considered as a very hackish approach (how to revert state properties once they have been already available for other feature?), we might not want to continue in that direction.
Implementation Details
Internal Design
The feature is essentially a composition of withResource and withEntities:
// PSEUDOCODE!!!
export function withEntityResource<T>(
  options: EntityOptions & {
    resource: ResourceOptions<T[]>;
  },
) {
  return signalStoreFeature(
    withEntities<T>(options), // internal call
    withResource<T[]>(options.resource, {
      onValue: (store, value) => {
        store.setAllEntities(value); // keeps entities in sync
      },
    }),
    withComputed((store) => ({
      value: () => store.entities(),
    })),
  );
}We cannot use withResource directly because the logic (see #desing-overview) doesn't fully match and will probably have to find a way to duplicate code or something else.
The same is true for withEntities. They need to be generated as linked Signals and not in the way how withEntities does it.
Also for named entities, there is much more type inference included.
Implementation Steps
This is a working draft that will evolve based on implementation challenges and feedback:
- Implement a prototype and provide a working Stackblitz example.
 - Give interested people the time to give it a try.
 - Decide to move on with final implementation or do another round, based on feedback.
 
Deliverables
It is possible to split the deliverables up into multiple PRs and also distribute the workload to several people.
- Tests
 - Basic Implementation
 - Demo
 - JSDoc
 - E2E Tests for Demo
 - Documentation (Docusaurus)