MeldUI

Data Table: useDataTableController

The parent-side composable that bundles sorting / filters / pagination refs and applies the filter→page and sort→page reset rules with flush:'sync'.

Overview

useDataTableController is the parent-side helper for <DataTable> and standalone <Filters> / <DataPagination> compositions. It owns the three v-model refs (sorting, filters, pagination), applies the filter→page and sort→page reset rule with flush: 'sync', and exposes a single merged state computed for the parent’s fetch watcher.

It has zero coupling to <DataTable> — you can use it for grid views, card lists, virtualized layouts, or any custom render path that needs the same three pieces of state.

Why the controller exists

There are three load-bearing rules that any DataTable parent needs to enforce:

  1. Reset to page 0 on filter change — otherwise a user filtering down to a tiny result set is stranded on a now-non-existent page 5.
  2. Reset to page 0 on sort change — matches industry behaviour (MUI DataGrid, AG Grid, PrimeVue) and avoids “page 5 of the new sort order contains arbitrary rows”.
  3. flush: 'sync' on those reset watchers — otherwise the parent’s watch(state, fetchPage) fires twice per user action: once with the new filter and the old pageIndex, then again after the reset lands.

Skip the controller and you must replicate all three rules yourself. The controller bundles them.

Options

OptionTypeDefaultDescription
pageSizenumber10Initial page size when initialPagination isn’t supplied
initialSortingSortingState[]Seed the sorting ref
initialFiltersDataTableFilterState{}Seed the filters ref
initialPaginationPaginationState{ pageIndex: 0, pageSize }Seed the pagination ref. Overrides pageSize when both are provided
resetPageOnFilterChangebooleantrueReset pageIndex to 0 whenever filters mutate
resetPageOnSortChangebooleantrueReset pageIndex to 0 whenever sorting mutates

Return value

MemberTypeDescription
sortingRef<SortingState>The sorting v-model — bind with v-model:sorting
filtersRef<DataTableFilterState>The filters v-model — bind with v-model:filters
paginationRef<PaginationState>The pagination v-model — bind with v-model:pagination
stateComputedRef<{ sorting, filters, pagination }>Merged read-only state. Watch with deep: true to fetch data
reset()() => voidReset all three refs to the initial values (or defaults if no initial values were supplied)

Single-fetch guarantee

When the user changes a filter while pagination.pageIndex > 0, two refs mutate: filters (the user’s change) and pagination (the controller’s page-reset side-effect). Without flush: 'sync', Vue would schedule the reset watcher to run later, which means the parent’s watch(state, fetchPage, { deep: true }) would fire twice — once with the new filter and stale pageIndex, then again after the reset.

flush: 'sync' lands the reset before the next microtask flushes, so the parent observes one coherent state and fires one fetch.

watch(state, fetchPage, { deep: true })

If you opt out of either reset (or skip the composable entirely), restore the rule yourself — see the manual snippet on Basic Usage.

Opting out of resets

Pass resetPageOnSortChange: false (or resetPageOnFilterChange: false) to preserve pageIndex across that axis. Generally don’t — the defaults match every major data-grid library.

const { sorting, filters, pagination, state } = useDataTableController({
  pageSize: 20,
  resetPageOnSortChange: false,
})

URL state restoration

Seed the controller from URL query params and persist back on every change. The composable doesn’t validate that a seeded pageIndex is within the filtered dataset’s bounds — that’s the parent’s job. After the first fetch resolves, clamp to pageCount - 1 if the URL was stale.

import { useRoute, useRouter } from 'vue-router'

const route = useRoute()
const router = useRouter()

const { sorting, filters, pagination, state } = useDataTableController({
  pageSize: Number(route.query.size) || 20,
  initialSorting: parseSorting(route.query.sort),
  initialFilters: parseFilters(route.query.f),
  initialPagination: {
    pageIndex: Number(route.query.page) || 0,
    pageSize: Number(route.query.size) || 20,
  },
})

watch(
  state,
  (s) => {
    router.replace({ query: serializeState(s) })
    fetchPage()
  },
  { deep: true },
)

See also

  • Basic Usage — the recommended setup using the controller.
  • Server-SidetableStateToServerParams and the response format.
  • Recipes — eight canonical wiring scenarios (internal, external, grid, URL state, switchable view).