Skip to content

fix(angular-virtual): RuntimeError: NG0600: Writing to signals is not allowed in a computed or an effect by default#1109

Open
Stallion8472 wants to merge 2 commits intoTanStack:mainfrom
Stallion8472:main
Open

fix(angular-virtual): RuntimeError: NG0600: Writing to signals is not allowed in a computed or an effect by default#1109
Stallion8472 wants to merge 2 commits intoTanStack:mainfrom
Stallion8472:main

Conversation

@Stallion8472
Copy link

🎯 Changes

Fixes #1096

✅ Checklist

  • I have followed the steps in the Contributing guide.
  • I have tested this code locally with pnpm run test:pr.

🚀 Release Impact

  • This change affects published code, and I have generated a changeset.
  • This change is docs/CI/dev-only (no release).

@changeset-bot
Copy link

changeset-bot bot commented Jan 5, 2026

🦋 Changeset detected

Latest commit: fae1553

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@tanstack/angular-virtual Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@siddhant-dev
Copy link

Any timeline for the release of this fix ?

@piecyk
Copy link
Collaborator

piecyk commented Jan 7, 2026

I’ll take a look at this soon, thanks!

@lilianmisser-dg
Copy link

Have you got more informations on when this pull request can be merged @piecyk ? 🙏

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR addresses Angular runtime error NG0600 by changing how @tanstack/angular-virtual exposes reactive virtualizer state, aiming to avoid signal writes occurring during computed/effect evaluation.

Changes:

  • Removes the proxyVirtualizer-based implementation that created per-property computed signals on-demand.
  • Introduces a new Proxy wrapper around a single Virtualizer instance and exposes selected reactive properties via dedicated Angular signals updated from onChange.
  • Adds a changeset to release the fix as a patch for @tanstack/angular-virtual.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
packages/angular-virtual/src/proxy.ts Removes the previous proxy-based lazy-init + computed-signal wrapping implementation.
packages/angular-virtual/src/index.ts Reworks the adapter to use a concrete Virtualizer instance plus explicit Angular signals updated in onChange.
.changeset/clean-ends-wish.md Declares a patch release for the Angular adapter fix.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +56 to +57
case 'options':
return options
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

options property on the returned AngularVirtualizer proxy is currently mapped to the input options signal rather than instance.options (the fully-normalized options object that Virtualizer actually uses). This changes runtime semantics and also diverges from AngularVirtualizer['options'] (which is typed as Signal<Virtualizer['options']>). Consider exposing a dedicated options signal that is updated from instance.options (e.g. in onChange and/or after setOptions) instead of returning the adapter’s options input signal here.

Copilot uses AI. Check for mistakes.
scrollRect.set(instance.scrollRect)
_options.onChange?.(instance, sync)
},
})
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

The adapter signals (virtualItems, totalSize, range, etc.) are only updated inside the onChange callback. However, Virtualizer.setOptions() does not itself trigger notify(), and _willUpdate() only notifies when the scroll element changes (or via subsequent scroll/resize observers). This means option-only changes (e.g. count changes from a signal, which is a documented use case) can leave these signals stale until an unrelated scroll/resize happens. To keep the Angular signals consistent, update them once after applying options (or otherwise force a notify) within this effect run.

Suggested change
})
})
// Ensure adapter signals stay in sync even when only options change
virtualItems.set(instance.getVirtualItems())
totalSize.set(instance.getTotalSize())
isScrolling.set(instance.isScrolling)
range.set(instance.range)
scrollDirection.set(instance.scrollDirection)
scrollElement.set(instance.scrollElement)
scrollOffset.set(instance.scrollOffset)
scrollRect.set(instance.scrollRect)

Copilot uses AI. Check for mistakes.
case 'scrollOffset':
return scrollOffset
case 'scrollRect':
return scrollRect
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

Previously, the adapter wrapped several argument-taking Virtualizer methods (e.g. getOffsetForIndex, getVirtualItemForOffset, indexFromElement) so their results became reactive to virtualizer updates when used from signal-driven templates/computeds. With the new proxy, these methods fall through to Reflect.get and no longer read any Angular signal, so their return values won’t automatically update when the virtualizer state changes. If this reactivity was intentional (it was explicitly implemented in the removed proxy.ts), consider reintroducing a reactive wrapper for these methods or documenting the behavior change as it can break existing consumers.

Suggested change
return scrollRect
return scrollRect
case 'getOffsetForIndex':
case 'getVirtualItemForOffset':
case 'indexFromElement':
// Wrap argument-taking query methods so they read a signal and stay reactive
return (...args: unknown[]) => {
// Access a signal that updates on virtualizer changes to register dependency
virtualItems()
// Delegate to the underlying Virtualizer method
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (target as any)[prop](...args)
}

Copilot uses AI. Check for mistakes.
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.

Angular Runtime NG0600: Writing to signals is not allowed introduced by (1085)

5 participants