Skip to content

Server-Side Plugin

The Server-Side plugin enables virtual scrolling with lazy loading from a remote API. It’s designed for large datasets (10,000+ rows) where loading all data upfront would be impractical or slow.

ScenarioRecommended Approach
< 1,000 rowsLoad all data upfront (no plugin needed)
1,000 - 10,000 rowsConsider based on network speed and data complexity
10,000+ rowsUse ServerSidePlugin
Infinite scroll / paginationUse ServerSidePlugin
Data changes frequently on serverUse ServerSidePlugin with refresh()
  • Block-based fetching: Loads only visible rows plus a buffer
  • LRU caching: Keeps recently viewed blocks in memory
  • Automatic prefetching: Loads next blocks as user scrolls
  • Concurrent request limiting: Prevents overwhelming the server
  • Loading placeholders: Shows loading state for pending rows
  • Integration with sorting/filtering: Works with sortHandler and filterHandler
import '@toolbox-web/grid/features/server-side';

The feature requires a data source that implements the getRows method. Set it via getPluginByName('serverSide') after the grid is ready:

import { queryGrid } from '@toolbox-web/grid';
import '@toolbox-web/grid/features/server-side';
const dataSource = {
async getRows(params) {
// params: { startRow, endRow, sortModel, filterModel }
const response = await fetch(`/api/data?start=${params.startRow}&end=${params.endRow}`);
const data = await response.json();
return {
rows: data.rows,
totalRowCount: data.total, // Required for scroll height calculation
};
},
};
const grid = queryGrid('tbw-grid');
grid.gridConfig = {
columns: [
{ field: 'id', header: 'ID' },
{ field: 'name', header: 'Name' },
{ field: 'email', header: 'Email' },
],
features: {
serverSide: { pageSize: 50 },
},
};
// Set data source after grid is ready
grid.ready().then(() => {
const plugin = grid.getPluginByName('serverSide');
plugin.setDataSource(dataSource);
});
Page size Rows fetched per request

Use the sortHandler config option for async sorting. When a user clicks a sortable column header, your handler is called instead of the built-in sorting logic. This is ideal for large datasets where sorting should happen on the backend.

grid.gridConfig = {
columns: [...],
sortHandler: async (rows, sortState, columns) => {
// sortState: { field: 'name', direction: 1 } // 1 = asc, -1 = desc
const response = await fetch(
`/api/data?sort=${sortState.field}&dir=${sortState.direction === 1 ? 'asc' : 'desc'}`
);
return response.json();
},
};

The handler receives:

  • rows - Current row array (may be useful for optimistic updates)
  • sortState - { field: string, direction: 1 | -1 }
  • columns - Column configurations (access sortComparator if needed)

Return the sorted array directly or a Promise that resolves to it.

For server-side filtering, use the FilteringPlugin’s async handlers. See the Filtering Plugin documentation for details on valuesHandler and filterHandler.

See ServerSideConfig for the full list of options and defaults.

const plugin = grid.getPluginByName('serverSide');
plugin.setDataSource(newDataSource); // Set or replace the data source
plugin.refresh(); // Reload current viewport from server
plugin.purgeCache(); // Clear all cached blocks
plugin.getTotalRowCount(); // Get server-reported total row count
plugin.isRowLoaded(index); // Check if a specific row is in cache
plugin.getLoadedBlockCount(); // Number of blocks currently cached

The plugin uses a block-based caching strategy:

┌────────────────────────────────────────────────┐
│ Total Dataset: 100,000 rows (on server) │
├────────────────────────────────────────────────┤
│ Block 0: rows 0-99 [CACHED] │
│ Block 1: rows 100-199 [CACHED] │
│ Block 2: rows 200-299 [LOADING...] │
│ Block 3: rows 300-399 [NOT LOADED] │
│ ... │
│ Block 999: rows 99900-99999 [NOT LOADED] │
└────────────────────────────────────────────────┘

Scroll triggers:

  1. onScroll event fires
  2. Plugin calculates which blocks are needed for the visible viewport
  3. Missing blocks are requested from the data source
  4. requestRender() is called when data arrives
  5. Grid re-renders with new data

Loading state: Rows that haven’t loaded yet are represented with placeholder objects:

{ __loading: true, __index: 42 }

You can detect loading rows via a custom cell renderer and style them accordingly:

columns: [{
field: 'name',
renderer: (value, row) => row.__loading ? '<span class="loading">Loading…</span>' : value,
}]

The ServerSidePlugin works well with:

PluginIntegration
FilteringPluginUse filterHandler for server-side filtering
MultiSortPluginUse sortHandler for server-side sorting
SelectionPluginWorks normally - selection state is client-side
EditingPluginWorks normally - edits are local until you sync
ExportPluginOnly exports cached rows by default

Note: RowGroupingPlugin and TreePlugin are not recommended with server-side data as they require all data to be present for grouping calculations.

AI assistants: For complete API documentation, implementation guides, and code examples for this library, see https://raw.githubusercontent.com/OysteinAmundsen/toolbox/main/llms-full.txt