Skip to content

fix(signals): keep computed values in sync in withPreviousValueOnLoading and withValueOnLoading - #5225

Open
gregorycode wants to merge 3 commits into
ngrx:mainfrom
gregorycode:fix/signals-previous-value-tracking
Open

gregorycode wants to merge 3 commits into
ngrx:mainfrom
gregorycode:fix/signals-previous-value-tracking

Conversation

@gregorycode

@gregorycode gregorycode commented Sep 22, 2026 •

Copy link
Copy Markdown

The issue: when a resource is extended with withPreviousValueOnLoading(), a computed that first reads resource.value() while the resource is loading stays undefined after the request resolves.

With this fix, the value proxy always reads the underlying value signal while keeping isLoading untracked, so consumers stay in sync and the previous value is 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?

[x] Bugfix
[ ] Feature
[ ] Code style update (formatting, local variables)
[ ] Refactoring (no functional changes, no api changes)
[ ] Build related changes
[ ] CI related changes
[ ] Documentation content changes
[ ] Other... Please describe:

What is the current behavior?

Closes #5224

What is the new behavior?

Does this PR introduce a breaking change?

[ ] Yes
[x] No

Other information

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>
@netlify

netlify Bot commented Sep 22, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for ngrx-io ready!

Built without sensitive environment variables

Name Link
🔨 Latest commit 877520c
🔍 Latest deploy log https://app.netlify.com/projects/ngrx-io/deploys/6ab32b91f671380008015d67
😎 Deploy Preview https://deploy-preview-5225--ngrx-io.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@rainerhahnekamp rainerhahnekamp left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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]);
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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);
});

Comment on lines 41 to 42
if (!resource.isLoading()) {
value = Reflect.apply(target, thisArg, args);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we always read the value signal and keep isLoading untracked? Please also retain the untracked import.

Suggested change
if (!resource.isLoading()) {
value = Reflect.apply(target, thisArg, args);
const currentValue = Reflect.apply(target, thisArg, args);
if (!untracked(resource.isLoading)) {
value = currentValue;

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure. Thanks.

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 rainerhahnekamp left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems ok to me. thanks

@gregorycode gregorycode changed the title fix(signals): track loading state in withPreviousValueOnLoading fix(signals): keep computed values in sync in withPreviousValueOnLoading Sep 22, 2026
timdeschryver
timdeschryver approved these changes Sep 23, 2026 •
@gregorycode gregorycode changed the title fix(signals): keep computed values in sync in withPreviousValueOnLoading fix(signals): keep computed values in sync in withPreviousValueOnLoading and withValueOnLoading Sep 24, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

withPreviousValueOnLoading: computed value remains undefined after initial request resolves

3 participants