ankurah-virtual-scroll
Platform-agnostic virtual scroll state machine with pagination for Ankurah.
Overview
ankurah-virtual-scroll provides smooth infinite scrolling through database-backed lists without loading everything into memory. It maintains a sliding window of items, expanding or sliding the window as the user scrolls, while preserving scroll position stability through intersection anchoring.
Features
- Bidirectional pagination: Load older and newer content seamlessly
- Scroll position stability: Maintain pixel-perfect scroll position when loading new items
- Reactive integration: Works with Ankurah's LiveQuery for real-time updates
- Platform-agnostic: Core logic in Rust with WASM and UniFFI bindings
- Variable item heights: Handles items of different sizes correctly
Installation
[]
= "0.7"
Usage
Leptos / Dioxus (Pure Rust)
Use ScrollManager<V> directly - no macro needed:
use ScrollManager;
let scroll_manager = new?;
// Start the scroll manager (fire and forget - runs initial query in background)
scroll_manager.start;
// Subscribe to visible set updates
let visible_set = scroll_manager.visible_set;
// Notify on scroll events
scroll_manager.on_scroll;
React Web (WASM) / React Native (UniFFI)
For JavaScript/TypeScript frontends, use the generate_scroll_manager! macro in your bindings crate to generate platform-specific wrappers:
use generate_scroll_manager;
// Generate MessageScrollManager with WASM or UniFFI bindings
generate_scroll_manager!;
This generates MessageScrollManager with the appropriate bindings based on feature flags:
wasmfeature: generates#[wasm_bindgen]bindings for React web appsuniffifeature: generates UniFFI bindings for React Native apps
React Component Example
import { useMemo, useCallback, useRef } from 'react'
import { signalObserver } from './utils'
import { MessageScrollManager, ctx } from './generated/bindings'
export const MessageList = signalObserver(function MessageList({ roomId }: { roomId: string }) {
const containerRef = useRef(null)
const lastScrollTopRef = useRef(0)
// Create scroll manager once per room
const manager = useMemo(() => {
const m = new MessageScrollManager(ctx(), `room = '${roomId}'`, 'timestamp DESC')
m.start() // Fire and forget
return m
}, [roomId])
// Get visible set signal (memoized)
const visibleSetSignal = useMemo(() => manager.visibleSet(), [manager])
// Call .get() inside signalObserver to auto-track reactivity
const visibleSet = visibleSetSignal.get()
const messages = visibleSet.items()
const handleScroll = useCallback((e: React.UIEvent) => {
const el = e.currentTarget
const scrollingUp = el.scrollTop < lastScrollTopRef.current
lastScrollTopRef.current = el.scrollTop
const topGap = el.scrollTop
const bottomGap = el.scrollHeight - el.scrollTop - el.clientHeight
manager.onScroll(topGap, bottomGap, scrollingUp)
}, [manager])
return (
{messages.map(msg => (
{msg.content()}
))}
)
})
Modes
- Live: At the newest edge, receiving real-time updates with auto-scroll
- Backward: User scrolled toward older items, loading historical content
- Forward: User scrolling back toward newer items, transitions to Live when reaching the edge
Architecture
The scroll manager handles:
- Query construction (predicate + cursor + ordering + limit)
- Mode tracking (Live / Backward / Forward)
- Boundary detection (at earliest/latest based on result count)
- Intersection anchoring for scroll stability
Platform layers handle:
- DOM/FlatList binding and scroll events
- Scroll position measurement and adjustment
- Spacer management
Crates
ankurah-virtual-scroll- Core scroll manager implementationankurah-virtual-scroll-derive- Derive macro for generating typed scroll managers
Version Compatibility
Minor versions align with ankurah (e.g., 0.7.x works with ankurah 0.7.x). Patch versions are independent.
License
MIT OR Apache-2.0