I've designed a store as shown below. It seems to connect the dots as described in this blog. All the console logs confirm that I'm passing the relevant lines. The issue is that the endpoint gets invoked but when I release the breakpoint in the controller, no update is available in the store. Somehow, the state is loading at the time of started execution and nothing happens when the execution is completed (i.e. when the promise is resolved). I've been looking and comparing for a few hours and I simply fail to see where I'm dropping the ball. I suspect it's the promise and async but I can't find how to "push it over the cliff" to the data available and notify the component that new info has arrived.
export interface Thing {
id: string;
name: string;
code: string;
}
export const ThingStore = signalStore(
withState<Thing>({ id: ZERO_GUID, name: "", code: "" } as Thing),
withProps(() => ({
_thingService: inject(ThingService),
})),
withProps((state) => ({
_thingResource: resource({
request: state.id,
//loader: async () => {
loader: () => {
console.log("loader for resource");
//return await state._thingService.load(state.id());
return state._thingService.load(state.id());
},
}),
})),
withComputed((state) => ({
thingComputed: computed(() => state._thingResource.value()),
loading: computed(() => state._thingResource.isLoading()),
})),
withMethods((state) => ({
init: (id: string) => {
console.log("init and patch state", id);
patchState(state, { id });
}
}))
);
The service executes the method (I can see the text in the console). However, like I mentioned, the call actually doesn't notify the component that it's done loading.
public async load(id: string): Promise<Thing> {
const url = `${this.baseUrl}/things`;
console.log("loading", id, url);
const request = this.httpClient.get<Thing>(id).pipe(take(1));
return await lastValueFrom(request);
}
I need help pin-pointing a relevant spot for further debug and preferably a suggestion on what I can add/remove to make it fetch the data. It's like the promise doesn't trigger change of state in the store. I'm confused. In the network tab, the call is shown and contains correct payload in the response. The effect fires but before I see the print from the service. I was expecting that when the promise is realized, then the loading flag is false, which (since it's a signal) will trigger the effect and re-update the data showing the fetched thing.
constructor() {
effect(() => {
this.store.init(this.id());
untracked(() => {
if (!this.store.loading())
console.log("you got some", this.store.thingComputed());
else console.log("No thing yet");
console.log("name by retry", this.store.nameComputed());
});
});
}
If you want to update the individual signals inside the state, you can use withMethods
and define an effect
which with patchState
the values from the resource
to the signal store state.
...
withHooks({
onInit(store) {
effect(() => {
const thingData = store._thingResource.value();
patchState(store, {
title: thingData.title,
completed: thingData.completed,
});
});
},
})
...
Because you are using untracked
inside the effect. The signal updates (The changes coming from this.store.loading()
, this.store.thingComputed()
and this.store.nameComputed()
) are not detected by the effect
(The signals inside the untracked are not tracked).
Execute an arbitrary function in a non-reactive (non-tracking) context. The executed function can, optionally, return a value.
If you comment out the untracked you code will work fine.
constructor() {
effect(() => {
this.store.init(this.id());
// untracked(() => {
if (!this.store.loading())
console.log('you got some', this.store.thingComputed());
else console.log('No thing yet');
// console.log("name by retry", this.store.nameComputed());
// });
});
}
A use case for untracked, is when you are updating a signal inside an effect
but you do not want the effect to fire again (infinite loop sometimes caused).
constructor() {
effect(() => {
const data = this.name();
// infinite loop due to below signal update
// this.name.set('infinite loop');
// use untracked to break infinite loop, by not tracking this update
untracked(() => {
this.name.set('no infinite loop');
});
});
}
import {
Component,
computed,
effect,
inject,
Injectable,
resource,
signal,
untracked,
} from '@angular/core';
import { DecimalPipe, JsonPipe } from '@angular/common';
import { bootstrapApplication } from '@angular/platform-browser';
import 'zone.js';
import { provideHttpClient, HttpClient } from '@angular/common/http';
import {
signalStore,
withState,
withProps,
withComputed,
withMethods,
patchState,
withHooks,
} from '@ngrx/signals';
export interface Thing {
id: string;
userId: string;
name: string;
title: string;
code: string;
completed: boolean;
}
@Injectable({ providedIn: 'root' })
export class ThingService {
http = inject(HttpClient);
load(id: any) {
return fetch(`https://jsonplaceholder.typicode.com/todos/${id}`).then(
(res) => res.json()
);
}
}
export const ThingStore = signalStore(
{ providedIn: 'root' },
withState<Thing>({ id: 'ZERO_GUID', title: '', code: '' } as Thing),
withProps(() => ({
_thingService: inject(ThingService),
})),
withProps((state) => ({
_thingResource: resource({
request: state.id,
loader: () => {
console.log('loader for resource');
return state._thingService.load(state.id());
},
}),
})),
withComputed((state) => ({
thingComputed: computed(() => state._thingResource.value()),
loading: computed(() => state._thingResource.isLoading()),
nameComputed: computed(() => state._thingResource.value()?.title),
})),
withMethods((state) => ({
init: (id: string) => {
console.log('init and patch state', id);
patchState(state, { id });
},
})),
withHooks({
onInit(store) {
effect(() => {
const thingData = store._thingResource.value();
patchState(store, {
title: thingData.title,
completed: thingData.completed,
});
});
},
})
);
@Component({
selector: 'app-root',
standalone: true,
imports: [JsonPipe],
template: `
<div>{{this.store.thingComputed() | json}}</div>
<div>{{this.store.title() | json}}</div>
<div>{{this.store.id() | json}}</div>
`,
})
export class App {
store = inject(ThingStore);
id = signal('1');
constructor() {
effect(() => {
this.store.init(this.id());
// untracked(() => {
if (!this.store.loading())
console.log('you got some', this.store.thingComputed());
else console.log('No thing yet');
console.log('name by retry', this.store.nameComputed());
console.log('title:', this.store.title());
console.log('id:', this.store.id());
// });
});
}
}
bootstrapApplication(App, { providers: [provideHttpClient()] });