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) | |
| } |
benjavicente
left a comment
There was a problem hiding this comment.
Hello 👋
I'm working on improving angular support from some of the libraries on the TanStack ecosystem. The Query adapter I'm working on has similar requirements and I can continue working in this issue if needed.
| function lazyInit() { | ||
| virtualizer ??= new Virtualizer(options()) | ||
| return virtualizer | ||
| const instance = new Virtualizer<TScrollElement, TItemElement>(options()) |
There was a problem hiding this comment.
You can't just call options() directly, since if that signal depends on Input signals, it will break with NG0952/NG0950. I wrote about it here for the Store adapter.
| scrollDirection.set(instance.scrollDirection) | ||
| scrollElement.set(instance.scrollElement) | ||
| scrollOffset.set(instance.scrollOffset) | ||
| scrollRect.set(instance.scrollRect) |
|
Hi @Stallion8472 Thanks again for opening this PR, really appreciate it! Since @benjavicente mentioned they’re working on improving Angular support across the TanStack ecosystem, I was wondering if you’d be comfortable with them picking this up and continuing the work here? |
🎯 Changes
Fixes #1096
✅ Checklist
pnpm run test:pr.🚀 Release Impact