Skip to content

Commit db3c5ad

Browse files
committed
add changeset
1 parent bcfe23e commit db3c5ad

File tree

2 files changed

+78
-0
lines changed

2 files changed

+78
-0
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@tanstack/virtual-core': patch
3+
---
4+
5+
Fix crash when component unmounts during `scrollToIndex` by adding a null guard for `targetWindow` inside the `requestAnimationFrame` callback

packages/virtual-core/tests/index.test.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,3 +158,76 @@ test('should update getTotalSize() when count option changes (filtering/search)'
158158

159159
expect(virtualizer.getTotalSize()).toBe(5000) // 100 × 50
160160
})
161+
162+
test('should not throw when component unmounts during scrollToIndex rAF loop', () => {
163+
// Collect rAF callbacks so we can flush them manually
164+
const rafCallbacks: Array<FrameRequestCallback> = []
165+
const mockRaf = vi.fn((cb: FrameRequestCallback) => {
166+
rafCallbacks.push(cb)
167+
return rafCallbacks.length
168+
})
169+
170+
const mockWindow = {
171+
requestAnimationFrame: mockRaf,
172+
ResizeObserver: vi.fn(() => ({
173+
observe: vi.fn(),
174+
unobserve: vi.fn(),
175+
disconnect: vi.fn(),
176+
})),
177+
}
178+
179+
const mockScrollElement = {
180+
scrollTop: 0,
181+
scrollLeft: 0,
182+
scrollWidth: 1000,
183+
scrollHeight: 5000,
184+
offsetWidth: 400,
185+
offsetHeight: 600,
186+
ownerDocument: {
187+
defaultView: mockWindow,
188+
},
189+
} as unknown as HTMLDivElement
190+
191+
const virtualizer = new Virtualizer({
192+
count: 100,
193+
estimateSize: () => 50,
194+
measureElement: (el) => el.getBoundingClientRect().height,
195+
getScrollElement: () => mockScrollElement,
196+
scrollToFn: vi.fn(),
197+
observeElementRect: (instance, cb) => {
198+
cb({ width: 400, height: 600 })
199+
return () => {}
200+
},
201+
observeElementOffset: (instance, cb) => {
202+
cb(0, false)
203+
return () => {}
204+
},
205+
})
206+
207+
// Initialize the virtualizer so targetWindow is set
208+
virtualizer._willUpdate()
209+
210+
// Populate elementsCache so isDynamicMode() returns true.
211+
// This triggers the code path where the rAF callback calls
212+
// this.targetWindow!.requestAnimationFrame(verify)
213+
const mockElement = {
214+
getBoundingClientRect: () => ({ height: 50 }),
215+
isConnected: true,
216+
setAttribute: vi.fn(),
217+
} as unknown as HTMLElement
218+
virtualizer.elementsCache.set(0, mockElement)
219+
220+
// Trigger scrollToIndex which schedules a rAF callback
221+
virtualizer.scrollToIndex(50)
222+
223+
// Simulate component unmount — cleanup sets targetWindow to null
224+
const unmount = virtualizer._didMount()
225+
unmount()
226+
227+
// Flush all pending rAF callbacks — this should not throw
228+
// Without the fix, this crashes with:
229+
// "Cannot read properties of null (reading 'requestAnimationFrame')"
230+
expect(() => {
231+
rafCallbacks.forEach((cb) => cb(0))
232+
}).not.toThrow()
233+
})

0 commit comments

Comments
 (0)