Spring
Spring

Reputation: 11

rxMethod must inject in a constructor context

Hi @all I have a Angular Ionic app and in this application my signalstore does make a lot of Problems:

import {
  patchState,
  signalStore,
  signalStoreFeature,
  type,
  withComputed,
  withHooks,
  withMethods,
  withState,
} from '@ngrx/signals';
import { HttpShoppingListService } from '../service/http-shopping-list.service';
import { computed, inject } from '@angular/core';
import { rxMethod } from '@ngrx/signals/rxjs-interop';
import { debounceTime, distinctUntilChanged, pipe, switchMap, tap } from 'rxjs';
import { tapResponse } from '@ngrx/operators';
import { ShoppingList } from '../domain/ShoppingList';
import { ShoppingListToCreate } from '../domain/ShoppingListToCreate';
import { Product } from '../domain/Product';
import { ShoppingListToUpdate } from '../domain/ShoppingListToUpdate';

export type Filter = {
  shoppingListId: string;
  order: 'asc' | 'desc';
};

export type ShoppingListState = {
  shoppingLists: ShoppingList[];
  isLoading: boolean;
  filter: Filter;
};

export const initialState: ShoppingListState = {
  shoppingLists: [],
  isLoading: false,
  filter: { shoppingListId: '', order: 'asc' },
};

export const ShoppingListStore = signalStore(
  { providedIn: 'root' },
  withState(initialState),
  // withComputed((store) => ({
  //   shoppingListCount: computed(() => store.shoppingLists().length),
  //   sortedShoppingLists: computed(() => store.shoppingLists().sort((a, b) => a.name.localeCompare( b.name ))),
  //   isLoading: computed(() => store.isLoading()),
  // })),

  withMethods(
    (store, httpShoppingListService = inject(HttpShoppingListService)) => ({
      loadShoppingListsByGroupId: rxMethod<string>(
        pipe(
          debounceTime(1000),
          distinctUntilChanged(),
          tap(() => patchState(store, { isLoading: true })),
          switchMap((groupId: string) =>
            httpShoppingListService.getAllShoppingLists(groupId).pipe(
              tapResponse({
                next: (shoppingLists: ShoppingList[]) =>
                  patchState(store, { shoppingLists }),
                error: (err) => {
                  console.error(err);
                },
                finalize: () => patchState(store, { isLoading: false }),
              })
            )
          )
        )
      ),

      addShoppingList: rxMethod<ShoppingListToCreate>(
        pipe(
          debounceTime(300),
          tap(() => patchState(store, { isLoading: true })),
          switchMap(shoppingListToCreate  =>
            httpShoppingListService.addShoppingList(shoppingListToCreate).pipe(
              tapResponse({
                next: (createdShoppingList: ShoppingList) => {
                  const test:ShoppingList[] = store.shoppingLists();
                  const updatedShoppingLists: ShoppingList[] = [
                      ...test,
                    createdShoppingList,
                  ]
                  patchState(store, { shoppingLists: updatedShoppingLists });
                },
                error: (err) => console.error(err),
                finalize: () => patchState(store, { isLoading: false }),
              })
            )
          )
        )
      ),

      addProductsToShoppingList: rxMethod<{
        shoppingListId: string;
        products: Product[];
      }>(
        pipe(
          distinctUntilChanged(),
          tap(() => patchState(store, { isLoading: true })),
          switchMap(({ shoppingListId, products }) =>
            httpShoppingListService
              .addProductsToShoppingList(shoppingListId, products)
              .pipe(
                tapResponse({
                  next: (updatedShoppingList: ShoppingList) => {
                    patchState(store, ({ shoppingLists }) => ({
                      shoppingLists: shoppingLists.map((list) =>
                        list.id === shoppingListId ? updatedShoppingList : list
                      ),
                    }));
                  },
                  error: (err) => console.error(err),
                  finalize: () => patchState(store, { isLoading: false }),
                })
              )
          )
        )
      ),

      updateShoppingList: rxMethod<{
        shoppingListToUpdate: ShoppingListToUpdate;
      }>(
        pipe(
          distinctUntilChanged(),
          tap(() => patchState(store, { isLoading: true })),
          switchMap(({ shoppingListToUpdate }) =>
            httpShoppingListService
              .updateShoppingList(shoppingListToUpdate)
              .pipe(
                tapResponse({
                  next: (updatedShoppingList: ShoppingList) => {
                    patchState(store, ({ shoppingLists }) => ({
                      shoppingLists: shoppingLists.map((list) =>
                        list.id === shoppingListToUpdate.id
                        ? updatedShoppingList
                        : list
                      ),
                    }));
                  },
                  error: (err) => {
                    console.error(err);
                  },
                  finalize: () => patchState(store, { isLoading: false }),
                })
              )
          )
        )
      ),
    })
  ),
);

It can not be inject in my component:

import { Component, inject, OnInit, signal, Signal } from '@angular/core';
import { IonicModule, NavController } from '@ionic/angular';
import { ShoppingListStore } from '../../store/shoppingListStore';

@Component({
  selector: 'app-group',
  templateUrl: './group.component.html',
  styleUrls: [ './group.component.scss' ],
  imports: [
    IonicModule
  ],
  standalone: true,
})
export class GroupComponent {

  readonly shoppingListStore = inject(ShoppingListStore);

  // activeIndex = 0;

  protected readonly groupId: Signal<string> = signal('1');

  // protected readonly shoppingLists: Signal<ShoppingList[]>;
  // protected readonly isLoading: Signal<boolean> = this.shoppingListStore.isLoading
  // protected readonly count: Signal<number> = this.shoppingListStore.shoppingListCount

  constructor() {
    this.shoppingListStore.loadShoppingListsByGroupId('1')
    // this.shoppingListStore.loadShoppingListsByGroupId('1');
    // this.shoppingLists = this.shoppingListStore.sortedShoppingLists;
    // @ts-ignore
    // this.a = at;
    // @ts-ignore

  }
}

In a normal angular Application, this store works but i dont know why its throw an error. Maybe because a module import ?:

    import { Component, inject, OnInit, signal } from '@angular/core';
    import { ActivatedRoute } from '@angular/router';
    import { IonButtons, IonContent, IonHeader, IonMenuButton, IonTitle, IonToolbar } from '@ionic/angular/standalone';
    import { NgComponentOutlet, NgIf } from '@angular/common';
    import { ComponentMapping } from '../domain/component-mapping';
    import { SideBarContentService } from '../service/side-bar-content.service';
    import { Folder } from '../domain/Folder';
    import { ShoppingListStore } from '../store/shoppingListStore';
    
    @Component({
      selector: 'app-folder',
      templateUrl: './folder.page.html',
      styleUrls: [ './folder.page.scss' ],
      standalone: true,
      providers: [ShoppingListStore],
      imports: [ IonHeader, IonToolbar, IonButtons, IonMenuButton, IonTitle, IonContent, NgComponentOutlet, NgIf ],
    })
    export class FolderPage implements OnInit {
    
      private activatedRoute = inject(ActivatedRoute);
      private sideBareContentService = inject(SideBarContentService);
    
      private allFolders = this.sideBareContentService.folders;
      currentFolderPage = signal<Folder | null>(null);
      component = signal<any | null>(null);
    
      private componentMapping: ComponentMapping = this.sideBareContentService.componentMapping;
    
      ngOnInit() {
        const folderId = this.activatedRoute.snapshot.paramMap.get('id');
        if (folderId) {
          const currentFolder = this.allFolders.find(folder => folder.id === folderId);
          if (currentFolder) {
            this.setFolder(currentFolder);
          }
        }
      }
      private setFolder(folder: Folder) {
        this.currentFolderPage.set(folder);
        this.loadComponent(folder);
      }
    
      private loadComponent(folder: Folder | null) {
        if( folder && this.componentMapping[ folder.id ] ) {
          this.componentMapping[ folder.id ]().then((component: any) => {
            this.component.set(component);
          });
        } else {
          this.component.set(null);
        }
      }
    }

import { AppPage } from '../domain/AppPage';
import { Observable, of } from 'rxjs';
import { ComponentMapping } from '../domain/component-mapping';
import { Folder } from '../domain/Folder';
import { DashboardComponent } from '../components/dashboard/dashboard.component';
import { GroupComponent } from '../components/group/group.component';

export class SideBarContentService {

  constructor() { }

  private _appPages: AppPage[] = [
    { sideBarName: 'Dashboard', url: '/folder/dashboard', icon: 'home-outline', isVisibleForUser: true },
    { sideBarName: 'Group Shopping Lists', url: '/folder/group', icon: 'cart-outline', isVisibleForUser: true },
    { sideBarName: 'PasswordManager', url: '/folder/password-manager', icon: 'key-outline', isVisibleForUser: true },
  ];

  private _componentMapping: ComponentMapping = {
    'dashboard': () => import('src/app/components/dashboard/dashboard.component').then(m => m.DashboardComponent),
    'group': () => import('src/app/components/group/group.component').then(m => m.GroupComponent),
  };

  private _folders: Folder[] = [
    {
      id: 'dashboard',
      title: 'Home',
      component: DashboardComponent,
    },
    {
      id: 'group',
      title: 'Shopping Lists for Group',
      component: GroupComponent,
    }
  ];

  get appPages(): Observable<AppPage[]> {
    return of(this._appPages);
  }

  get folders(): Folder[] {
    return this._folders;
  }

  get componentMapping() : ComponentMapping {
    return this._componentMapping;
  }
}

Following Error message occours:

ERROR RuntimeError: NG0203: rxMethod() can only be used within an injection context such as a constructor, a factory function, a field initializer, or a function used with `runInInjectionContext`. Find more at https://angular.io/errors/NG0203
    at assertInInjectionContext (core.mjs:3324:11)
    at rxMethod (ngrx-signals-rxjs-interop.mjs:5:5)
    at shoppingListStore.ts:50:35
    at ngrx-signals.mjs:211:21
    at ngrx-signals.mjs:87:62
    at Array.reduce (<anonymous>)
    at new _SignalStore (ngrx-signals.mjs:87:35)
    at Object.SignalStore_Factory [as factory] (ngrx-signals.mjs:116:14)
    at core.mjs:3136:35
    at runInInjectorProfilerContext (core.mjs:867:5)
handleError @ core.mjs:7195
next @ core.mjs:33183
ConsumerObserver2.next @ Subscriber.js:90
Subscriber2._next @ Subscriber.js:59
Subscriber2.next @ Subscriber.js:32
(anonym) @ Subject.js:41
errorContext @ errorContext.js:23
Subject2.next @ Subject.js:31
emit @ core.mjs:6590
(anonym) @ core.mjs:7082
invoke @ zone.js:339
run @ zone.js:110
runOutsideAngular @ core.mjs:6944
onHandleError @ core.mjs:7082
handleError @ zone.js:342
runGuarded @ zone.js:124
api.microtaskDrainDone @ zone.js:2235
drainMicroTaskQueue @ zone.js:541
invokeTask @ zone.js:444
invokeTask @ zone.js:1086
globalCallback @ zone.js:1116
globalZoneAwareCallback @ zone.js:1147
22 weitere Frames anzeigen
Weniger anzeigen

I think the rxMethod is being implemented correctly... Does anyone have any ideas?

Upvotes: 0

Views: 122

Answers (2)

BarakD
BarakD

Reputation: 3

From the docs:

By default, the rxMethod needs to be executed within an injection context. It's tied to its lifecycle and is automatically cleaned up when the injector is destroyed.

Initialization of the reactive method outside an injection context is possible by providing an injector as the second argument to the rxMethod function.

Your not using the rxMethod() in the right context.

The solution is to inject the current Injector and pass it to the rxMethod()


withMethods(
(store, httpShoppingListService = inject(HttpShoppingListService),injector = inject(Injector)//inject the current injector) => ({
  loadShoppingListsByGroupId: rxMethod<string>(
    pipe(
      debounceTime(1000),
      distinctUntilChanged(),
      tap(() => patchState(store, { isLoading: true })),
      switchMap((groupId: string) =>
        httpShoppingListService.getAllShoppingLists(groupId).pipe(
          tapResponse({
            next: (shoppingLists: ShoppingList[]) =>
              patchState(store, { shoppingLists }),
            error: (err) => {
              console.error(err);
            },
            finalize: () => patchState(store, { isLoading: false }),
          })
        )
      )
    ,{ injector})//pass the injector to the rx method
  ),

Upvotes: 0

Aria
Aria

Reputation: 11

Provide ShoppingListStore in your component

providers: [ShoppingListStore],

Upvotes: 0

Related Questions