Vue Data Grid Server-Side Data

Server-side data

Delegate sorting, filtering and pagination to your API with a single dataProvider function — the Data Grid fetches one debounced request per state change and drops stale responses.

Client-side mode ends where the browser’s memory does. When your dataset lives in a database with tens of thousands of rows or more, hand the work to your backend: set data-provider and the grid stops computing row models locally and asks your API for each page instead. This is the single biggest adoption unlocker for large data.

Server-side data

Set dataProvider and the grid switches to server-side mode — every sorting, filtering or pagination change triggers one debounced request (stale responses are dropped automatically), a loading overlay covers the viewport and totals come from your totalRows. Server-side mode implies pagination. This live demo hits CoreUI’s public demo API (apitest.coreui.io/demos/users) — 10,000+ records sorted, filtered and paged server-side.

vue
<script setup lang="ts">
import { CDataGrid } from '@coreui/vue-data-grid'
import type { DataGridDataRequest } from '@coreui/vue-data-grid'

const dataProvider = async ({ sorting, columnFilters, pagination }: DataGridDataRequest) => {
  const params = new URLSearchParams({
    offset: String(pagination.pageIndex * pagination.pageSize),
    limit: String(pagination.pageSize)
  })

  for (const { id, value } of columnFilters) {
    params.append(id, String(value))
  }

  const [sort] = sorting
  if (sort) {
    params.append('sort', `${sort.id}%${sort.desc ? 'desc' : 'asc'}`)
  }

  const response = await fetch(`https://apitest.coreui.io/demos/users?${params}`)
  const result = await response.json()
  const totalRows = Number(result.number_of_matching_records)

  return { items: totalRows ? result.records : [], totalRows }
}

const columns = [
  { key: 'first_name', label: 'First name' },
  { key: 'last_name', label: 'Last name' },
  { key: 'email', label: 'Email' },
  { key: 'country', label: 'Country' },
  { key: 'ip_address', label: 'IP' }
]
</script>

<template>
  <CDataGrid
    :columns="columns"
    :data-provider="dataProvider"
    :item-key="(item) => String(item.id)"
    column-filters
  />
</template>

The dataProvider contract

<script setup>
const dataProvider = async ({ sorting, columnFilters, globalFilter, pagination }) => {
  // fetch from your API and return the matching page
  return { items, totalRows }
}
</script>

<template>
  <CDataGrid :columns="columns" :data-provider="dataProvider" />
</template>
  • One request per state change. sorting, columnFilters, globalFilter and pagination arrive as structured state; return the page of items plus the full totalRows count so the pager can render.
  • Debounced & race-safe. Rapid changes coalesce into one request and only the latest response is applied — stale ones are dropped by request id.
  • Loading UX. A .data-grid-loading overlay with a spinner covers the viewport and aria-busy is set while a request is in flight (labels.loading).
  • Errors. A rejected fetch emits data-error with the error, shows the empty state (labels.loadError) and leaves the grid interactive.
  • Success. Each load emits data-load with { items, totalRows }.

Selection semantics

row-selection is keyed by itemKey, so a selection survives page changes by design. The selection-change payload contains only the items present in the current page’s data — the grid does not cache full objects for pages it has scrolled past.

The same page-bound rule applies to CSV export: in server-side mode every scope (filtered, all, selected) exports the currently loaded page only.

When the filter menu is used, a column’s entry in columnFilters carries the structured value — { conditions: [{ operator, value, value2? }], join: 'and' | 'or' } for typed operators or { operator: 'in', value: [...] } for the set filter — instead of a plain string. Servers should handle both shapes.