diff --git a/packages/core/src/resource/resource.ts b/packages/core/src/resource/resource.ts index af50c84510d..c503aef3f4f 100644 --- a/packages/core/src/resource/resource.ts +++ b/packages/core/src/resource/resource.ts @@ -130,15 +130,19 @@ abstract class BaseWritableResource implements WritableResource { readonly isLoading = computed(() => this.status() === 'loading' || this.status() === 'reloading'); - hasValue(): this is ResourceRef> { - // 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> { + return this.isValueDefined(); } asReadonly(): Resource { diff --git a/packages/core/test/resource/resource_spec.ts b/packages/core/test/resource/resource_spec.ts index d1bf617eaba..e640ba15336 100644 --- a/packages/core/test/resource/resource_spec.ts +++ b/packages/core/test/resource/resource_spec.ts @@ -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({ + 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);