Skip to main content

TanStack Virtual's New Chat Primitives Make Scroll Bookkeeping Boring

· 3 min read
Gergely Sipos
Frontend Architect

TanStack Virtual added chat-specific primitives in Tanner Linsley's May 25 announcement. Standard virtualization is start-anchored; chat UIs need end-anchored, prepend-safe, stream-aware scroll management. These primitives move that bookkeeping from hand-rolled state into a tested library.

Why Chat Lists Are Different

Standard virtual lists assume you're appending to the bottom — the top anchor stays stable, and the visible window tracks downward. Chat UIs invert every assumption:

  • Bottom-anchor stability — when a user scrolls up to load history, prepending rows must not shift their current reading position.
  • Follow-on-append — new messages should auto-scroll only when the user is already at the bottom, not unconditionally.
  • Dynamic heights — AI responses grow token by token as they stream in, continuously invalidating estimated row sizes.
  • Stable identity — rows need stable keys so prepended history doesn't cause the virtualizer to confuse old and new items.

Getting this right from scratch means wiring up scroll listeners, mutation observers, and a handful of boolean flags — all things that are easy to get slightly wrong.

The New Primitives

TanStack Virtual adds a handful of config options and methods that cover the chat-specific surface area directly:

const virtualizer = useVirtualizer({
count: messages.length,
getScrollElement: () => scrollRef.current,
estimateSize: () => 80,
anchorTo: 'end',
followOnAppend: true,
scrollEndThreshold: 80,
getItemKey: (index) => messages[index].id,
});

When the user has scrolled away from the bottom, a "Jump to Latest" button can use:

if (!virtualizer.isAtEnd()) {
virtualizer.scrollToEnd();
}

One detail worth calling out explicitly: getItemKey must return a stable message ID, not the array index. Without this, prepending history causes the virtualizer to misidentify existing items and lose scroll position. It's easy to miss and hard to debug.

For paginated history loading, useInfiniteQuery from TanStack Query pairs naturally — trigger the next page when the user scrolls near the top, and prepend results into the message list.

What This Means for Our AI Chat Cookbook

The AI Assistant Chat cookbook currently puts scroll anchor state in Zustand — scrollAnchor, autoscroll flags, and a conditional effect that fires scrollIntoView on new messages.

TanStack Virtual's primitives replace that manual bookkeeping piece by piece:

  • isAtEnd() replaces the "is user pinned to bottom?" boolean in the store.
  • followOnAppend replaces the conditional auto-scroll effect.
  • anchorTo: 'end' replaces the scrollIntoView + mutation observer dance triggered on prepend.

The resulting stack has clean separation: useChat (Vercel AI SDK) owns the message list and streaming state, TanStack Query handles paginated history, and TanStack Virtual handles rendering and scroll position — each tool does exactly one job.

tip

If you're building from the cookbook, TanStack Virtual doesn't replace Zustand — it shrinks what Zustand needs to own. Keep composer draft, sidebar state, and thread selection in the store; let the virtualizer own scroll position.