fix(signals): keep computed values in sync in withPreviousValueOnLoading and withValueOnLoading - #5225
fix(signals): keep computed values in sync in withPreviousValueOnLoading and withValueOnLoading#5225gregorycode wants to merge 3 commits into
Conversation
Read `isLoading` in a tracked context in the `value` proxy of `withPreviousValueOnLoading`. Previously, `isLoading` was read with `untracked`, so a computed that first read `value()` while loading recorded no dependencies and stayed stuck on the cached value after the request resolved. Closes ngrx#5224 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
✅ Deploy Preview for ngrx-io ready!Built without sensitive environment variables
To edit notification comments on pull requests, go to your Netlify project configuration. |
rainerhahnekamp
left a comment
There was a problem hiding this comment.
I suggested an alternative approach where we only track the value and not also the isLoading signal. Let me know what you think.
| await resolveWithValue([1, 2, 3]); | ||
| expect(value()).toEqual([1, 2, 3]); | ||
| }); | ||
|
|
There was a problem hiding this comment.
Could we also cover a loading-only change? A consumer of value() should not recompute when reload() starts and the value stays the same, but it should recompute when a new value arrives. This test passes locally with the implementation suggested below.
| it('does not recompute consumers when only the loading state changes', async () => { | |
| const { resource, initLoading, reload, resolveWithValue } = | |
| createTestResource<number[]>(); | |
| withPreviousValueOnLoading().apply(resource); | |
| initLoading(); | |
| await resolveWithValue([1, 2, 3]); | |
| const readValue = vi.fn(() => resource.value()); | |
| const value = computed(readValue); | |
| expect(value()).toEqual([1, 2, 3]); | |
| expect(readValue).toHaveBeenCalledTimes(1); | |
| reload(); | |
| expect(resource.isLoading()).toBe(true); | |
| expect(value()).toEqual([1, 2, 3]); | |
| expect(readValue).toHaveBeenCalledTimes(1); | |
| await resolveWithValue([4, 5]); | |
| expect(value()).toEqual([4, 5]); | |
| expect(readValue).toHaveBeenCalledTimes(2); | |
| }); | |
| if (!resource.isLoading()) { | ||
| value = Reflect.apply(target, thisArg, args); |
There was a problem hiding this comment.
Could we always read the value signal and keep isLoading untracked? Please also retain the untracked import.
| if (!resource.isLoading()) { | |
| value = Reflect.apply(target, thisArg, args); | |
| const currentValue = Reflect.apply(target, thisArg, args); | |
| if (!untracked(resource.isLoading)) { | |
| value = currentValue; |
Always read the underlying `value` signal in the `value` proxy and keep `isLoading` untracked. Consumers of `value()` no longer recompute when only the loading state changes, but still recompute when a new value arrives.
rainerhahnekamp
left a comment
There was a problem hiding this comment.
seems ok to me. thanks
The issue: when a resource is extended with
withPreviousValueOnLoading(), a computed that first readsresource.value()while the resource is loading staysundefinedafter the request resolves.With this fix, the
valueproxy always reads the underlyingvaluesignal while keepingisLoadinguntracked, so consumers stay in sync and the previousvalueis still returned while loading.PR Checklist
Please check if your PR fulfills the following requirements:
PR Type
What kind of change does this PR introduce?
What is the current behavior?
Closes #5224
What is the new behavior?
Does this PR introduce a breaking change?
Other information