Grid state inspector

Mount a dev-only side panel that shows the live grid state - filters, selection, active cell, column order, pinning, visibility - updated every animation frame. The thing every dev wishes was always-on while debugging.

When

You're stuck on one of:

Sticking console.log(api.getXxx()) everywhere works once. A panel that polls every reader every frame answers all of these at once.

The contract

The SvGridApi exposes a snapshot getter for every piece of public state you might care about:

Reader What it returns
getFilters() Per-column operator-filter clauses
getSelectedRowIds() Array of row ids currently selected
getSelected() Cell selection ranges [r0,c0,r1,c1][]
getActiveCell() The focused cell, or null
getColumnOrder() Column ids in current visual order
getColumnPinning() { left: [], right: [] }
getColumns() Every column with id, header, visible
getDisplayedRows() The post-pipeline visible rows
getData() The pre-pipeline data array

Sort state is event-driven only (onSortingChange) - there is no getSorting() getter. The pattern below tracks it locally in the panel to fill the gap.

Implementation

<script lang="ts">
  import {
    SvGrid, tableFeatures,
    rowSortingFeature, columnFilteringFeature,
    rowSelectionFeature, rowExpandingFeature,
    type SvGridApi, type ColumnDef,
  } from 'sv-grid-community'

  type Row = { id: number; name: string; team: string; score: number; active: boolean }
  const rows: Row[] = [/* ... */]

  const features = tableFeatures({
    rowSortingFeature, columnFilteringFeature,
    rowSelectionFeature, rowExpandingFeature,
  })
  const columns: ColumnDef<typeof features, Row>[] = [/* ... */]

  let api = $state<SvGridApi<typeof features, Row> | null>(null)

  type Snapshot = {
    filters: unknown
    selectedRowIds: unknown
    activeCell: unknown
    columnOrder: unknown
    columnPinning: unknown
    columns: unknown
    displayedRowCount: number
    cellSelection: unknown
  }
  let snap = $state<Snapshot | null>(null)
  let running = $state(true)

  // Track sort locally - the api has no getSorting() reader.
  let sortField = $state<string | null>(null)
  let sortDesc  = $state(false)

  $effect(() => {
    if (!running) return
    let raf = 0
    function tick() {
      if (api) {
        snap = {
          filters:           api.getFilters(),
          selectedRowIds:    api.getSelectedRowIds(),
          activeCell:        api.getActiveCell(),
          columnOrder:       api.getColumnOrder(),
          columnPinning:     api.getColumnPinning(),
          columns:           api.getColumns().map((c) => ({
            id: c.id, header: c.header, visible: c.visible,
          })),
          displayedRowCount: api.getDisplayedRows().length,
          cellSelection:     api.getSelected(),
        }
      }
      raf = requestAnimationFrame(tick)
    }
    raf = requestAnimationFrame(tick)
    return () => cancelAnimationFrame(raf)
  })

  // "Click a leaf to mutate it back" - quick mutators that drive the
  // grid api from the panel. Sort goes asc → desc → off.
  function flipSort(field: string) {
    if (!api) return
    if (sortField === field) {
      if (!sortDesc) { sortDesc = true; api.setSort(field, 'desc') }
      else { sortField = null; api.clearSort() }
    } else {
      sortField = field; sortDesc = false
      api.setSort(field, 'asc')
    }
  }
  function clearAll() {
    if (!api) return
    api.clearSort()
    api.clearAllFilters()
    sortField = null; sortDesc = false
  }
</script>

<button onclick={() => flipSort('score')}>Flip score sort</button>
<button onclick={clearAll}>Clear</button>
<button onclick={() => (running = !running)}>
  {running ? '⏸ Pause' : '▶ Resume'}
</button>

<SvGrid
  data={rows}
  {columns}
  {features}
  enableCellSelection
  onApiReady={(a) => (api = a)}
/>

<aside class="devtools">
  <h3>Grid state · live</h3>
  {#if snap}
    {#each Object.entries(snap) as [k, v] (k)}
      <section>
        <span class="key">{k}</span>
        <pre><code>{JSON.stringify(v, null, 2)}</code></pre>
      </section>
    {/each}
  {/if}
</aside>

Polling vs subscribing

This recipe polls every frame for simplicity - 16ms cadence, no subscriptions to manage. For most dev panels that's fine; the cost is a few JSON.stringify calls per frame.

If you want event-driven instead, hook the grid's on* callbacks:

<SvGrid
  ...
  onSortingChange={(sorting) => log('sort', sorting)}
  onFiltersChange={(filters) => log('filter', filters)}
  onCellSelectionChange={(ranges) => log('cellsel', ranges)}
  onColumnOrderChange={(order) => log('colorder', order)}
/>

Event-driven catches every mutation without missing intermediate states that polling might skip over. Polling catches state changes that don't have a callback (e.g. the active cell moving via keyboard).

A real devtools panel combines both: poll on rAF for the current snapshot, plus a separate event log seeded from the callbacks.

Don't ship to production

Wrap behind an env flag:

{#if import.meta.env.DEV || localStorage.getItem('svgrid:devtools') === 'on'}
  <Devtools />
{/if}

Polling every frame and serialising the whole snapshot is fine in dev, unnecessary in prod.

Demo

The runnable companion: Demo 135 - Devtools panel (gallery) - this panel running next to a real grid.

See also