Reputation: 10810
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
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
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
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
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