Two grids, one shared state

createGridState returns a [get, set] tuple - a reactive store you own. Feed it to two createSvGrid engines and they stay in lockstep.

A live, editable Svelte 5 data grid example. Open the interactive demo or read the documentation.

What this example shows

`createGridState` returns a [get, set] tuple - a reactive store you own. Feed the SAME store to two independent createSvGrid engines and they stay in lockstep: sort a column in Grid A and Grid B re-sorts too, because both read the same reactive `sorting`. Each renders its own markup.

Source code (189-headless-shared-state.svelte)

<!-- Documented in: docs/help/headless/controlled-state.md -->
<script lang="ts">
  /**
   * 189. Two headless grids, one shared state
   * -----------------------------------------
   * `createGridState` returns a [get, set] tuple - a reactive store you own.
   * Feed the SAME store to two independent createSvGrid engines and they stay in
   * lockstep: sort a column in Grid A and Grid B re-sorts too, because both read
   * the same reactive `sorting`. Each renders its own markup.
   */
  import {
    createSvGrid,
    createCoreRowModel,
    createSortedRowModel,
    createGridState,
    tableFeatures,
    rowSortingFeature,
    type ColumnDef,
  } from '@svgrid/grid'

  type Row = { name: string; region: string; deals: number }
  const features = tableFeatures({ rowSortingFeature })
  const columns: ColumnDef<typeof features, Row>[] = [
    { field: 'name', header: 'Rep' },
    { field: 'region', header: 'Region' },
    { field: 'deals', header: 'Deals' },
  ]
  const data: Row[] = [
    { name: 'Ava', region: 'EMEA', deals: 42 },
    { name: 'Liam', region: 'APAC', deals: 51 },
    { name: 'Noah', region: 'AMER', deals: 33 },
    { name: 'Mia', region: 'EMEA', deals: 60 },
    { name: 'Zoe', region: 'APAC', deals: 28 },
  ]

  // One shared, reactive store - owned here, not by either grid.
  type Sort = { id: string; desc: boolean }
  const [getSorting, setSorting] = createGridState<Sort[]>([{ id: 'deals', desc: true }])

  // Plain builder - reads the shared store on each call.
  const buildGrid = () =>
    createSvGrid({
      _features: features,
      _rowModels: { coreRowModel: createCoreRowModel<Row>(), sortedRowModel: createSortedRowModel<Row>() },
      data,
      columns,
      state: { sorting: getSorting() },              // read the shared store
      onSortingChange: (u: unknown) => setSorting(u as never), // write the shared store
      enableSorting: true,
    } as never)
  // Two independent engines, both derived from the same shared store.
  const gridA = $derived.by(buildGrid)
  const gridB = $derived.by(buildGrid)

  function toggleSort(id: string) {
    const cur = getSorting()[0]
    setSorting(cur?.id !== id ? [{ id, desc: false }] : cur.desc ? [] : [{ id, desc: true }])
  }
  const ind = (id: string) => {
    const s = getSorting()[0]
    return s?.id === id ? (s.desc ? '▼' : '▲') : ''
  }
</script>

<section class="ss-wrap">
  <p class="ss-note">
    Both grids share one <code>createGridState</code> store. Click any header in
    <strong>either</strong> grid - the other re-sorts in lockstep. Two engines,
    one source of truth.
  </p>

  <div class="ss-grids">
    {#each [{ label: 'Grid A', g: gridA }, { label: 'Grid B', g: gridB }] as panel (panel.label)}
      <div class="ss-panel">
        <div class="ss-title">{panel.label}</div>
        <table>
          <thead>
            {#each panel.g.getHeaderGroups() as hg (hg.id)}
              <tr>
                {#each hg.headers as h (h.id)}
                  <th class:num={h.column.id === 'deals'} onclick={() => toggleSort(h.column.id)}>
                    {h.column.columnDef.header}<span class="ss-ind">{ind(h.column.id)}</span>
                  </th>
                {/each}
              </tr>
            {/each}
          </thead>
          <tbody>
            {#each panel.g.getRowModel().rows as r (r.id)}
              {@const row = r.original as Row}
              <tr>
                <td>{row.name}</td>
                <td>{row.region}</td>
                <td class="num">{row.deals}</td>
              </tr>
            {/each}
          </tbody>
        </table>
      </div>
    {/each}
  </div>
</section>

<style>
  .ss-wrap { display: flex; flex-direction: column; gap: 14px; padding: 4px; }
  .ss-note { font-size: 13px; color: var(--sg-fg, #0f172a); margin: 0; }
  .ss-note strong { color: var(--sg-accent, #6366f1); }
  .ss-grids { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
  @media (max-width: 640px) { .ss-grids { grid-template-columns: 1fr; } }
  .ss-panel { border: 1px solid var(--sg-border, #e2e8f0); border-radius: 10px; overflow: hidden; }
  .ss-title {
    font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em;
    color: var(--sg-muted, #64748b); padding: 8px 12px; background: var(--sg-header-bg, #f8fafc);
    border-bottom: 1px solid var(--sg-border, #e2e8f0);
  }
  .ss-panel table { border-collapse: collapse; width: 100%; font-size: 13px; color: var(--sg-fg, #0f172a); }
  .ss-panel th {
    text-align: left; padding: 8px 12px; cursor: pointer; user-select: none; font-weight: 700;
    border-bottom: 1px solid var(--sg-border, #e2e8f0);
  }
  .ss-panel th:hover { color: var(--sg-accent, #6366f1); }
  .ss-ind { color: var(--sg-accent, #6366f1); margin-left: 5px; font-size: 10px; }
  .ss-panel td { padding: 8px 12px; border-bottom: 1px solid var(--sg-border, #eef2f7); }
  .ss-panel td.num, .ss-panel th.num { text-align: right; font-variant-numeric: tabular-nums; }
</style>

View this example on GitHub

Related documentation

More Headless examples

  • Headless -> your own table - No <SvGrid>: the createSvGrid engine sorts + filters, and this component renders a plain, hand-styled <table>. The engine does the logic; you own the markup.
  • Headless virtualization - 50,000 rows, headless. createSvelteVirtualizer reports the visible slice; the markup is hand-written in a custom scroll container.
  • Styling a headless table - You own every pixel. Same engine, three looks - flip preset (minimal / bordered / card), density, and zebra striping. --sg-* tokens keep it in sync with the site theme.
  • Row models are a pipeline - Flip the group-by control and watch the pipeline change shape: core -> grouped -> expanded. Group rows carry the aggregate: sum roll-up; the markup is a plain hand-styled <table>.
  • Headless server-side - Paging + sorting + filtering + load on demand. The "server" owns the data and returns one page at a time; each state change fires a single request. The engine wraps only the current page.