angulartypescriptngrxngrx-storeangular-signals

Setting custom primary key with NgRx signal store withEntities


Problem

I have an ngrx/signalStore that contains fruits as entities. For proper usage of the signalStore, you need to have an id field in the Fruit class, but the API I use retrieves fruits that have a field fruitId. Is there a way to tell the store that it will be the primary key for the class instead of introducing a duplicated field named id?

Here is my store:

export const FruitStore = signalStore(
  { providedIn: 'root' },
  withEntities<Fruit>(),
  withMethods((
    store,
    fruitService = inject(FruitService),
  ) => ({
    loadFruits: rxMethod<void>(
      pipe(
        exhaustMap(() => {
          return fruitService.getFruits().pipe(
            tapResponse({
              next: (fruits) => {
                patchState(store, setAllEntities(fruits);
              },
              error: (error: { message: string }) => {
                console.log(error.message);
              }
            }),
          );
        }),
      )
    ),
  })),
  withHooks({
    onInit({ loadFruits }) {
      loadFruits();
    }
  }),
);

My Fruit class:

export class Fruit {
  fruitId: number;
  name: string;
  imageURL: string;
}

Current solution

My current solution is to add a duplicated field to the class (Fruit class comes from the API generated with OpenAPI, so I can not change it). But I'm not fully satisfied with this solution. Do you have any ideas to improve it?

export type WithId<T> = T & { id: number };

Changes in the store:

export const FruitStore = signalStore(
  { providedIn: 'root' },
  // Extend Fruit class 'WithId'
  withEntities<WithId<Fruit>>(),
  withMethods((
    store,
    fruitService = inject(FruitService),
  ) => ({
    loadFruits: rxMethod<void>(
      pipe(
        exhaustMap(() => {
          return fruitService.getFruits().pipe(
            tapResponse({
              next: (fruits) => {
                // Add the duplicated field
                patchState(store, setAllEntities(fruits.map(fruit => ({ ...fruit, id: fruit.fruitId })));
              },
              error: (error: { message: string }) => {
                console.log(error.message);
              }
            }),
          );
        }),
      )
    ),
  })),
  withHooks({
    onInit({ loadFruits }) {
      loadFruits();
    }
  }),
);

Solution

  • I went looking for the answer to this exact question, and I found what I feel is a better answer. This might be a newly added capability, but here it is...

    Ngrx entityConfig

    const todoConfig = entityConfig({
      entity: type<Todo>(),
      collection: 'todo', // changes ids, entityMap, and entities to todoIds, todoEntityMap, and todoEntities.
      selectId: (todo) => todo.key, // <-- define it once, then reuse it
    });
    
    export const TodosStore = signalStore(
      withEntities(todoConfig),
      withMethods((store) => ({
        addTodo(todo: Todo): void {
          patchState(store, addEntity(todo, todoConfig));
        },
        removeTodo(todo: Todo): void {
          patchState(store, removeEntity(todo, todoConfig));
        },
      }))
    );