Infinity Scroll
The Infinity Scroll feature enables dynamic data loading as users scroll through the grid, efficiently managing large datasets by loading data in chunks and cleaning up unused data to optimize memory usage.
Key Features
Section titled “Key Features”- Dynamic data loading based on scroll position
- Pre-loads data ahead of scroll direction
- Efficient memory management by cleaning up off-screen data
- Configurable chunk size and buffer sizes
- Support for both known and unknown total data sizes
- Compatible with pinned top and bottom rows
Loading...Basic Setup
Section titled “Basic Setup”To enable infinity scroll in your grid:
import { InfinityScrollPlugin } from '@revolist/revogrid-pro';
const grid = document.createElement('revo-grid');grid.plugins = [InfinityScrollPlugin];grid.pinnedTopSource = [{ id: 'status', name: 'Pinned status row' }];grid.pinnedBottomSource = [{ id: 'summary', name: 'Pinned summary row' }];
grid.infinityScroll = { chunkSize: 50, // Number of rows per chunk bufferSize: 100, // How many rows to keep in buffer preloadThreshold: 0.75, // When to start loading more data (0-1) total: 1000, // Optional: total number of rows loadData: async (skip, limit, order, singleConditionFilters) => { // Fetch data from your API const response = await fetch( `/api/data?skip=${skip}&limit=${limit}&order=${JSON.stringify(order)}&filter=${JSON.stringify(singleConditionFilters)}`, ); const data = await response.json(); return data; // You can return an array or { data, hasMore, total } },};The configuration is an observable grid property, so assigning a new object to
grid.infinityScroll updates the plugin. In Vue templates, use the kebab-case
alias with Vue’s .prop modifier so the object is assigned as an element
property:
<RevoGrid :plugins="[InfinityScrollPlugin]" :infinity-scroll.prop="infinityScroll"/>additionalData.infinityScroll remains available as a deprecated compatibility
fallback for existing applications.
Pinned rows stay outside the remote loading lifecycle. Use the core
pinnedTopSource and pinnedBottomSource grid properties for static status,
summary, or action rows while InfinityScrollPlugin manages only the main
scrollable source.
Configuration Options
Section titled “Configuration Options”| Option | Type | Default | Description |
|---|---|---|---|
chunkSize | number | undefined | Number of rows to load in each chunk. Can be set dynamically by the grid. |
bufferSize | number | undefined | Number of rows to keep in the memory buffer. Can be set dynamically by the grid. |
preloadThreshold | number | 0.75 | When to trigger loading the next chunk (0–1). |
total | number | undefined | Total number of rows. If omitted, the plugin grows the source as you scroll. |
loadData | function | Required | Function that loads data chunks. Return . |
query | InfinityScrollConfig[‘query’] | Optional | Native polling, caching, retries and shared sources. See configuration types and defaults . |
Remote advanced filtering
Section titled “Remote advanced filtering”For a new backend integration, use the canonical filterAst and
executionContext arguments. The tree is JSON-safe and preserves nested
groups, cross-column logic, structured operators, and quick-filter composition.
The context captures the single reference instant, timezone, and serializable
blank-value policy used by relative-date rules, so the server can evaluate the
same request as the grid.
loadData: async ( skip, limit, order, _singleConditionFilters, _multiConditionFilters, _quickFilter, _temporalContext, filterAst, executionContext, context, // Optional tenth argument: { signal }.) => { const response = await fetch('/api/data', { method: 'POST', signal: context?.signal, headers: { 'content-type': 'application/json' }, body: JSON.stringify({ page: { skip, limit }, order, filter: filterAst ? { filterAst, executionContext } : undefined, }), }); return response.json();};signal: context?.signal forwards the optional cancellation signal to fetch,
so the transport can stop when the read is no longer needed. See
loader context and cancellation.
Register AdvanceFilterPlugin alongside InfinityScrollPlugin before using
this contract. Its filterastchange event and the beforefilterapply detail
expose the same canonical pair; see Canonical Filter AST
for the tree schema and Filter Events
for a custom remote adapter.
Compatibility arguments
Section titled “Compatibility arguments”loadData keeps the established positional arguments for existing callers.
The fourth singleConditionFilters argument contains at most one condition per
column. The optional fifth multiConditionFilters argument contains every
advanced column condition and its relation; use it when an existing backend
supports multiple conditions for one column. The sixth argument is the
normalized global quick-filter payload, and the seventh is temporal transport
data for older date/datetime integrations. The eighth and ninth arguments are
the canonical filterAst and executionContext shown above.
multiConditionFilters can contain runtime-only values such as a selection
filter Set, so do not serialize it unchanged in new integrations. If an
existing backend requires it, normalize those values at that adapter boundary.
For example, if the Name column must contain Remote and must not contain
Archived, the fifth argument keeps both conditions and their relation:
{ name: [ { id: 0, type: 'contains', value: 'Remote', relation: 'and' }, { id: 1, type: 'notContains', value: 'Archived', relation: 'and' }, ],}The fourth argument remains compatible with single-condition backends and, for
this example, contains only { name: { type: 'contains', value: 'Remote' } }.
Use multiConditionFilters whenever the backend needs the full expression.
Using hasMore for unknown total
Section titled “Using hasMore for unknown total”When your backend does not provide a fixed total, return hasMore from
loadData. This is the most reliable way to tell the plugin when to stop
requesting additional chunks.
grid.infinityScroll = { chunkSize: 100, loadData: async (skip, limit, order, singleConditionFilters) => { const response = await fetch('/api/data', { method: 'POST', body: JSON.stringify({ skip, limit, order, filter: singleConditionFilters, }), }); const result = await response.json(); return { data: result.items, hasMore: result.hasMore, }; },};Dynamic total from backend
Section titled “Dynamic total from backend”You can also return total from loadData. When provided, the plugin updates
its internal total size and uses it for source sizing and end detection.
Memory Management
Section titled “Memory Management”The plugin automatically manages memory by:
- Loading new chunks of data as the user scrolls
- Maintaining a buffer of rows before and after the visible area
- Cleaning up data that’s far from the current viewport
Exporting to Excel
Section titled “Exporting to Excel”ExportExcelPlugin exports grid source rows, not rendered DOM rows. Ordinary
client-side virtualization therefore exports the entire source, including rows
currently outside the viewport.
Infinity scroll is different: it keeps only a moving client-side window and may
represent remote, unloaded positions with placeholders. Export does not call
loadData repeatedly and cannot discover records that have never been loaded.
To prevent a misleading partial workbook, direct export now fails with an
actionable error while InfinityScrollPlugin is active.
If you need a complete workbook, fetch the export dataset from the same backend
API and pass those rows to ExportExcelPlugin instead of relying on the visible
infinite-scroll grid. The demo export button uses a temporary hidden grid with
the same columns so the visible grid is not disturbed while the workbook is
generated.
Best Practices
Section titled “Best Practices”-
Choose Appropriate Chunk Size
- Smaller chunks mean more frequent loading but less memory usage
- Larger chunks mean fewer API calls but more memory usage
-
Configure Buffer Size
- Larger buffers provide smoother scrolling but use more memory
- Smaller buffers save memory but might cause more loading events
-
Optimize Data Loading
- Implement server-side pagination in your API
- Return only necessary data fields
- Consider data compression for large datasets
-
Handle Loading States
- Show loading indicators during data fetching
- Handle errors gracefully
- Consider implementing retry logic for failed requests
Example with Loading State
Section titled “Example with Loading State”grid.infinityScroll = { chunkSize: 50, bufferSize: 100, loadData: async (skip, limit, order, singleConditionFilters) => { try { // Show loading state const response = await fetch( `/api/data?skip=${skip}&limit=${limit}&order=${JSON.stringify(order)}&filter=${JSON.stringify(singleConditionFilters)}`, ); const data = await response.json(); // Hide loading state return data; } catch (error) { console.error('Failed to load data:', error); return []; // Return empty array on error } finally { // Hide loading state } },};This implementation provides efficient handling of large datasets while maintaining optimal performance and user experience.
Built-in shared block queries
Section titled “Built-in shared block queries”Keep grid.infinityScroll.loadData(skip, limit, ...) as the loader. It now runs
through independent Query block observers automatically; no connection or deferred
loading setup is needed. All existing callback positions and return shapes remain.
An optional cancellation context { signal } is appended as argument ten. Query owns freshness,
read retries and
deduplication; the plugin owns viewport demand and sparse placement.
The optional query configuration supports a shared source, reconcile, onStatus,
resource and snapshot consistency. Create shared scopes with
createDataQueryContext from @revolist/revogrid-pro. Default consistency is eventual;
providers need deterministic ordering with a unique tie-breaker. Insert/delete/order
changes require source invalidation, or source.accepted() after canonical writes.
Snapshot providers must pin one revision across blocks/counts and validate it through
query.consistency.getRevision.
Distant jumps request their destination/overscan blocks directly. Observers and
editable rows outside the viewport buffer plus three chunks on either side are
released; placeholder indices and total length remain stable. Query expires inactive
entries after the scope’s gcTime. Selection remains positional. Destroying one
view releases its subscriptions without cancelling another view’s shared read.
Query usage and configuration
Section titled “Query usage and configuration”See Server Data Queries for polling,
shared views,
caching,
retries,
lifecycle and the complete configuration types and defaults.
Your existing loader callback stays unchanged; add options inside query.
The query concepts are explained individually:
- Pages, blocks, routes and observers
- Polling, mount, focus and reconnect refetch
staleTimefreshness andgcTimecache retentionenabled, offline pauses andnetworkModeretry,retryDelayandretryOnMountmetaand callback policies- Contexts, sources, resources, deduplication and concurrency
- Invalidation, accepted writes and acceptance fences
- Eventual/snapshot consistency and deterministic ordering
- Pending edits, reconciliation, hydration and status
- Cancellation and destruction
For an application-owned query library/cache, set query: false. See
bypassing Query
for lifecycle behavior and the difference from query: { enabled: false }.