Francesco
Francesco

Reputation: 10810

Manage ngrx signal store reactivity in component class

After updating to Angula v17, I am rewriting a legacy component using the "classic" ngrx (observables based) with the new ngrx/signals store.

I defined some selectors in the following fashion:

withComputed((state) => ({
    selectOrderId: computed(() => state.orderId()),
    selectAvailableAgents: computed(() => state.availableAgents())
    ... 
})

However, for the lazy nature of signals, if I use these selectors in the component class (not template), they will not be updated dynamically if the value changes:

const orderId = this.ordersStore.selectOrderId();
// orderId has only the current (at the time of invocation) value from the store

If orderId is null, I need to invoke a service to fetch it, return the value to the component and update the store as well. Eventually, to keep this logic together, I can implement this logic inside the withMethod that returns returns the value as an observable.
This way I would skip the selector call altogether:

     withMethods((state, ordersService = inject(OrdersService)) => ({

            fetchOrderId$: () => {
                const orderId = state.orderId();

                // If orderId available, do not call the API
                if (orderId) {
                    return of(orderId);
                }

                return ordersService.getOrderId()
                    .pipe(
                        tapResponse({
                            next: (orderId) => {
                                patchState(state, { orderId })
                            },
                            error: (error: HttpErrorResponse) => {
                                console.error("An error occurred: ", error.message);
                            }
                        })
                    )
            }

            //In component class:

            this.orderStore.fetchOrderId$()
                .pipe(
                  takeUntil(this.destroy$)
                )
                .subscribe({
                  next: (orderId) => { 
                       // Here the logic using orderId
                  }
                )}

Using this approach, though, seems to defeat a bit the way signals work.

In this scenario, I could think of effect() as the only way to indirectly detect when a specific part of the signal state is changed and apply specific actions to the component, for instance. But Angular docs suggest not using them to propagate the state, to avoid potential issues.

Therefore, the resulting approach loses the peculiar indirectness provided by ngrx with observables:

For the cases where a value is not needed immediately, I will use rxMethods to update the state and re-execute if their params change.

Is this a valid approach to managing selectors and values with ngrx signals or am I missing anything?

Upvotes: 2

Views: 4877

Answers (4)

behruz
behruz

Reputation: 588

You can do it without relying to much on observables because now you have signals. You can modify your codebase to convert the service response to a promise, and then update the store based on the service value.

withMethods((state, ordersService = inject(OrdersService)) => ({
    async fetchOrderId: () => {
       const orderId = state.orderId();

       if (orderId) {
          return orderId;
       }

       const orderId = await lastValueFrom(ordersService.getOrderId());

       if(orderId) {
          patchState(state, { orderId })
       }                              
    }

In component class:

this.orderStore.fetchOrderId()

wherever you need the value:

this.orderStore.orderId()

Upvotes: 0

Gabriel Guerrero
Gabriel Guerrero

Reputation: 496

I can think of two ways, but first the main warning about setting signals inside effects is to avoid creating infinite loops, but if you know what you are doing, I think it is fine to do so, in your case, you could do a withHooks something like

withHooks((fetchOrderId, orderId)=> {}),
  onInit: ()=> {
   effect(()=> {
     if (orderId() == null) { // == null will block the infinite loop
       fetchOrderId() // fetch the order and sets the order id in the store
     }
    }), {allowSignalWrites: true});

  }
}

Another way is using an rxMethod for your fetchOrderId, this is similar to the previous answer, but here I'm setting the value in the store, which is ok because it's not inside an effect, and you get the benefit of using the orderId later in other computations

const s = signalStore(
  withState<{orderId?: string}>({}),
  withMethods((store, servise = inject(OrderService)) =>{
    fetchOrderId: rxMethod<string>(
      pipe(filter(v => v == null),
            exhaustMap(() => servise.getOrderId()
              .pipe(tapResponse({
                      next: (orderId) => {
                        patchState(state, { orderId })
                      },
                      error: (error: HttpErrorResponse) => {
                        console.error("An error occurred: ", error.message);
                      }
                    })
              ))))
  })
  withHooks((fetchOrderId, orderId)=> {}),
    onInit: ()=> {
      fetchOrderId(orderId)
  }

Notice this time in the withHooks on init fetchOrderId(orderId) receives the signal as a reference, not as a value, rxMethod can receive a value or a signal or an observable, if is one of the last two, it will subscribe to them, so the fetchOrderId will run every time orderId changes.

It is important to notice the second approach could also cause an infinite loop, is the filter(v => v == null) that prevents it.

Upvotes: 0

thomasbee
thomasbee

Reputation: 31

Would this work? In your component, define an rxMethod

readonly ensureOrderId = rxMethod<number>(pipe(tap(orderId => {
    if (orderId === null) {
        this.ordersService.getOrderId(); // ensure state is also patched somehow
    }
})))

Connect this method with the signal in ngOnInit

this.ensureOrderId(this.ordersStore.selectOrderId)

Note the missing brackets, i.e. selectOrderId rather than selectOrderId(). By passing the signal rather than the value, ensureOrderId() is executed whenever orderId changes.

Upvotes: 2

Tabea
Tabea

Reputation: 93

Just to make sure that you didn't make the same mistake I made: When you select a signal from the signal store, do not call it (i.e. add ()). If you do so, the signal is only evaluated once and you don't have the handle of the signal but the value of whatever was in that signal. You should only call/ evaluate the signal when you really want the value. For example in your html, if you want to display the current value

Upvotes: 2

Related Questions