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

  • When working with angular 17, you need to specify the idKey property in the second argument object of setAllEntities.

      ...
      withMethods((store) => {
        const todoService = inject(TodoService);
        return {
          loadTodos: rxMethod<void>(
            pipe(
              exhaustMap(() => {
                return todoService.getTodos().pipe(
                  tap((fruits) => {
                    // Add the duplicated field
                    patchState(
                      store,
                      setAllEntities<Todo>(
                        fruits.map((fruit) => ({ ...fruit })),
                        { idKey: 'todoId' }
                      )
                    );
                  })
                );
              })
            )
          ),
          ...
    

    Stackblitz Demo -> 4th tab in the application.


    When you are working with latest version of angular (17+). The process is present in the documentation.

    NGRX Docs - Custom Entity Identifier

    import { patchState, signalStore, withMethods } from '@ngrx/signals';
    import {
      addEntities,
      removeEntity,
      SelectEntityId,
      setEntity,
      updateAllEntities,
      withEntities,
    } from '@ngrx/signals/entities';
    
    type Todo = {
      key: number;
      text: string;
      completed: boolean;
    };
    
    const selectId: SelectEntityId<Todo> = (todo) => todo.key;
    
    export const TodosStore = signalStore(
      withEntities<Todo>(),
      withMethods((store) => ({
        addTodos(todos: Todo[]): void {
          patchState(store, addEntities(todos, { selectId }));
        },
        setTodo(todo: Todo): void {
          patchState(store, setEntity(todo, { selectId }));
        },
        completeAllTodos(): void {
          patchState(
            store,
            updateAllEntities({ completed: true }, { selectId })
          );
        },
        removeTodo(key: number): void {
          patchState(store, removeEntity(key));
        },
      }))
    );