mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
fix(core): fix change tracking for Resource#hasValue (#62595)
When using `hasValue()` I would expect it to behave like any other reactive value such that changes to the internal `value()` that do not cause `hasValue()` to return anything different do not trigger change detection, but this was not the case. This change wraps the value checking in a `computed` such that it behaves as expected again while still preserving the type narrowing. PR Close #62595
This commit is contained in:
committed by
Kirill Cherkashin
parent
ba8a5d826e
commit
2c522efbe5
@@ -130,15 +130,19 @@ abstract class BaseWritableResource<T> implements WritableResource<T> {
|
||||
|
||||
readonly isLoading = computed(() => this.status() === 'loading' || this.status() === 'reloading');
|
||||
|
||||
hasValue(): this is ResourceRef<Exclude<T, undefined>> {
|
||||
// Note: we specifically read `isError()` instead of `status()` here to avoid triggering
|
||||
// reactive consumers which read `hasValue()`. This way, if `hasValue()` is used inside of an
|
||||
// effect, it doesn't cause the effect to rerun on every status change.
|
||||
// Use a computed here to avoid triggering reactive consumers if the value changes while staying
|
||||
// either defined or undefined.
|
||||
private readonly isValueDefined = computed(() => {
|
||||
// Check if it's in an error state first to prevent the error from bubbling up.
|
||||
if (this.isError()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.value() !== undefined;
|
||||
});
|
||||
|
||||
hasValue(): this is ResourceRef<Exclude<T, undefined>> {
|
||||
return this.isValueDefined();
|
||||
}
|
||||
|
||||
asReadonly(): Resource<T> {
|
||||
|
||||
@@ -236,6 +236,44 @@ describe('resource', () => {
|
||||
expect(effectRuns).toBe(1);
|
||||
});
|
||||
|
||||
it('should not trigger consumers on every value change via hasValue()', async () => {
|
||||
const testResource = resource<number | undefined, unknown>({
|
||||
loader: () => Promise.resolve(undefined),
|
||||
injector: TestBed.inject(Injector),
|
||||
});
|
||||
|
||||
let effectRuns = 0;
|
||||
effect(
|
||||
() => {
|
||||
testResource.hasValue();
|
||||
effectRuns++;
|
||||
},
|
||||
{injector: TestBed.inject(Injector)},
|
||||
);
|
||||
|
||||
TestBed.tick();
|
||||
// Starts off without a value
|
||||
expect(testResource.hasValue()).toBeFalse();
|
||||
// Effect should run the first time.
|
||||
expect(effectRuns).toBe(1);
|
||||
|
||||
// Set value to something defined
|
||||
testResource.set(0);
|
||||
TestBed.tick();
|
||||
// Value is now defined
|
||||
expect(testResource.hasValue()).toBeTrue();
|
||||
// The effect should have been re-run.
|
||||
expect(effectRuns).toBe(2);
|
||||
|
||||
// Set value to something else defined
|
||||
testResource.set(1);
|
||||
TestBed.tick();
|
||||
// Value is still defined
|
||||
expect(testResource.hasValue()).toBeTrue();
|
||||
// The effect should not rerun.
|
||||
expect(effectRuns).toBe(2);
|
||||
});
|
||||
|
||||
it('should update computed signals', async () => {
|
||||
const backend = new MockEchoBackend();
|
||||
const counter = signal(0);
|
||||
|
||||
Reference in New Issue
Block a user