Levente Vági
Levente Vági

Reputation: 43

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();
    }
  }),
);

Upvotes: 2

Views: 368

Answers (1)

Naren Murali
Naren Murali

Reputation: 56054

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));
    },
  }))
);

Upvotes: 3

Related Questions