Platform-agnostic virtual scroll state machine with pagination for Ankurah.
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.
- Bidirectional pagination: Load older and newer content seamlessly
- Scroll position stability: Maintain scroll position when loading new items via intersection anchoring
- Reactive integration: Works with Ankurah's LiveQuery for real-time updates
- Platform-agnostic: Core logic in Rust with WASM bindings (UniFFI in development)
- Variable item heights: Handles items of different sizes correctly
[dependencies]
ankurah-virtual-scroll = "0.7"Use ScrollManager<V> directly - no macro needed:
/// Example: Creating and using a ScrollManager
#[allow(dead_code)]
async fn scroll_manager_example() -> Result<(), Box<dyn std::error::Error>> {
let ctx = durable_sled_setup().await?;
// Create scroll manager with full configuration
let scroll_manager = ScrollManager::<TestMessageView>::new(
&ctx,
"true", // Filter predicate (e.g., "room = 'general'")
"timestamp DESC", // Display order
40, // Minimum row height (pixels)
2.0, // Buffer factor (2.0 = 2x viewport)
600, // Viewport height (pixels)
)?;
// Initialize (runs initial query)
scroll_manager.start().await;
// Read visible items from the signal
let visible_set = scroll_manager.visible_set().get();
for item in &visible_set.items {
// Access item fields via the View trait
let _id = item.entity().id();
}
// Notify on scroll events with first/last visible EntityIds
if let (Some(first), Some(last)) = (visible_set.items.first(), visible_set.items.last()) {
let first_visible_id = first.entity().id();
let last_visible_id = last.entity().id();
let scrolling_backward = true; // true = scrolling toward older items
scroll_manager.on_scroll(first_visible_id, last_visible_id, scrolling_backward);
}
Ok(())
}For JavaScript/TypeScript frontends, use the generate_scroll_manager! macro in your bindings crate to generate platform-specific wrappers:
// Generate MessageScrollManager with WASM bindings
ankurah_virtual_scroll::generate_scroll_manager!(
Message,
MessageView,
MessageLiveQuery,
timestamp_field = "timestamp"
);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 (in development)
import { useEffect, useRef, useState, useCallback, useMemo } from 'react'
// Types from WASM bindings (generated by ankurah-virtual-scroll-derive)
interface MessageView {
id: () => { toString: () => string }
text: () => string
}
interface MessageVisibleSet {
items: MessageView[]
}
interface MessageVisibleSetSignal {
get: () => MessageVisibleSet
}
interface MessageScrollManager {
start: () => Promise<void>
visibleSet: () => MessageVisibleSetSignal
onScroll: (firstVisible: string, lastVisible: string, scrollingBackward: boolean) => void
}
// These would come from your WASM bindings package
declare function ctx(): unknown
declare const MessageScrollManager: new (
ctx: unknown,
predicate: string,
orderBy: string,
minRowHeight: number,
bufferFactor: number,
viewportHeight: number
) => MessageScrollManager
const VIEWPORT_HEIGHT = 400
const MIN_ROW_HEIGHT = 40
export function ExampleMessageList({ roomId }: { roomId: string }) {
const containerRef = useRef<HTMLDivElement>(null)
const lastScrollTop = useRef(0)
const [items, setItems] = useState<MessageView[]>([])
// Create scroll manager once per room
const manager = useMemo(() => {
return new MessageScrollManager(
ctx(),
`room = '${roomId}'`,
'timestamp DESC',
MIN_ROW_HEIGHT,
2.0,
VIEWPORT_HEIGHT
)
}, [roomId])
// Initialize and sync state on mount
useEffect(() => {
manager.start().then(() => {
const vs = manager.visibleSet().get()
setItems([...vs.items])
})
}, [manager])
// Find first/last visible items by checking DOM element positions
const findVisibleItems = useCallback(() => {
const container = containerRef.current
if (!container) return null
const elements = container.querySelectorAll('[data-item-id]')
let firstId: string | null = null
let lastId: string | null = null
elements.forEach(el => {
const rect = el.getBoundingClientRect()
const containerRect = container.getBoundingClientRect()
// Item is visible if it overlaps with container viewport
if (rect.bottom > containerRect.top && rect.top < containerRect.bottom) {
const id = el.getAttribute('data-item-id')
if (id) {
if (!firstId) firstId = id
lastId = id
}
}
})
return firstId && lastId ? { firstId, lastId } : null
}, [])
// Handle scroll events - detect direction and notify scroll manager
const handleScroll = useCallback((e: React.UIEvent<HTMLDivElement>) => {
const el = e.currentTarget
const scrollingBackward = el.scrollTop < lastScrollTop.current
lastScrollTop.current = el.scrollTop
const visible = findVisibleItems()
if (visible) {
// Pass EntityId strings (not pixel values) to scroll manager
manager.onScroll(visible.firstId, visible.lastId, scrollingBackward)
// Sync state after potential window slide
const vs = manager.visibleSet().get()
setItems([...vs.items])
}
}, [manager, findVisibleItems])
return (
<div
ref={containerRef}
onScroll={handleScroll}
style={{ height: VIEWPORT_HEIGHT, overflowY: 'auto' }}
>
{items.map(msg => (
<div key={msg.id().toString()} data-item-id={msg.id().toString()}>
{msg.text()}
</div>
))}
</div>
)
}- 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
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
- Visible item detection (by EntityId)
- Scroll position measurement and adjustment
ankurah-virtual-scroll- Core scroll manager implementationankurah-virtual-scroll-derive- Derive macro for generating typed scroll managers
Minor versions align with ankurah (e.g., 0.7.x works with ankurah 0.7.x). Patch versions are independent.
MIT OR Apache-2.0