-
Notifications
You must be signed in to change notification settings - Fork 26.6k
Description
Which @angular/* package(s) are relevant/related to the feature request?
core
Description
Currently, the computed operator for signal provides the ability to create a new signal value, which is dependent on other signals.
The limitation I want to solve is that currently, the computed operator only allows for synchronous computations with immediate resolution of the newly computed value.
My suggestion is to allow computed signals to accept an async value (promise or observable), which when combined with a flag would wait until an actual value returns from the async value before updating the signal.
Proposed solution
Use case -
@Component({
template: `
<input (change)="query.set($event.value)">
`,
})
class TestComponent {
query = signal('');
}
Consider the use case in the component above, where on every keystroke I want to fetch some data from a backend service.
Example - Promise:
@Component({
template: `
<input (change)="query.set($event.value)">
`,
})
class TestComponent {
query = signal('');
service = inject(DataService);
data:Signal<Data | undefined> = computed(
() => this.service.queryAsPromise({query: this.query()}), // returns a promise
{ async: true }
)
}
Example - Observable:
@Component({
template: `
<input (change)="query.set($event.value)">
`,
})
class TestComponent {
query = signal('');
service = inject(DataService);
data:Signal<Data | undefined> = computed(
() => this.service.queryAsObservable({query: this.query()}), // returns an observable
{ async: true }
)
}
Using the computed operator with an async callback, but without specifying the async flag would result in a signal that holds an async value (same behavior today so not a breaking change):
@Component({
template: `
<input (change)="query.set($event.value)">
`,
})
class TestComponent {
query = signal('');
service = inject(DataService);
data:Signal<Promise<Data>> = computed(
() => this.service.queryAsPromise({query: this.query()}),
)
}
Alternatives considered
Currently this is possible only with these 2 options:
1 - Using promise and effect which writes to a new signal:
@Component({
template: `
<input (change)="query.set($event.value)">
`,
})
class TestComponent {
query = signal('');
data = signal([]);
constructor(service:DataService) {
effect(async () => {
const query = this.query()
const result = await service.queryAsPromise({query});
this.data.set(result);
}, { allowSignalWrites: true })
}
}
2 - Using observable and jumping back and forwards from signal and observables to merge all required data (not to mention the added complexity if you have several dependencies for your async request):
@Component({
template: `
<input (change)="query.set($event.value)">
`,
})
class TestComponent {
query = signal('');
service = inject(DataService);
data = toSignal(
toObservable(this.query)
.pipe(mergeMap(query => this.service.queryAsObservable({query})))
);
}
Both solutions are very verbose, convoluted, and seems like bending the framework to do something it wasn't supposed to in terms of ergonomics (my opinion).
The usecase seems pretty trivial, and I come across it constantly in our projects, so much so that I created a new function just to wrap this logic (toSignalWithDeps).