fix(angular-virtual): RuntimeError: NG0600: Writing to signals is not allowed in a computed or an effect by default#1109
fix(angular-virtual): RuntimeError: NG0600: Writing to signals is not allowed in a computed or an effect by default#1109Stallion8472 wants to merge 2 commits intoTanStack:mainfrom
Conversation
…puted or an effect by default by simplifying proxy
🦋 Changeset detectedLatest commit: fae1553 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
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 |
|
Any timeline for the release of this fix ? |
|
I’ll take a look at this soon, thanks! |
|
Have you got more informations on when this pull request can be merged @piecyk ? 🙏 |
There was a problem hiding this comment.
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
Proxywrapper around a singleVirtualizerinstance and exposes selected reactive properties via dedicated Angular signals updated fromonChange. - 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.
| case 'options': | ||
| return options |
There was a problem hiding this comment.
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.
| scrollRect.set(instance.scrollRect) | ||
| _options.onChange?.(instance, sync) | ||
| }, | ||
| }) |
There was a problem hiding this comment.
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.
| }) | |
| }) | |
| // 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) |
| case 'scrollOffset': | ||
| return scrollOffset | ||
| case 'scrollRect': | ||
| return scrollRect |
There was a problem hiding this comment.
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.
| 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) | |
| } |
🎯 Changes
Fixes #1096
✅ Checklist
pnpm run test:pr.🚀 Release Impact