Transactions

api.applyTransaction({ add, update, remove }) applies a batch of row mutations in a single data update - one re-render for the whole batch, not one per row. It's the path to use for high-frequency / streaming feeds (WebSocket deltas, tick data) where calling addRow / setCellValue per row would thrash.

Open the live example: Transaction API (batched) (Real-time & Streaming)

const result = api.applyTransaction({
  add:    [newOrder],                          // appended
  update: [{ ...order, price: nextPrice }],    // matched by id
  remove: ['ORD-1001', staleRowRef],           // by id OR row reference
})
// result -> { added: 1, updated: 1, removed: 2 }

Matching

The call returns the counts actually applied, so you can log or reconcile.

Why batch

Each applyTransaction produces exactly one new data array and therefore one reactive update, regardless of how many rows changed. On a live feed that emits dozens of deltas per second, batching them per animation frame (or per WebSocket message) keeps the grid smooth where per-row calls would not.

// buffer deltas, flush once per frame
let buffer: Delta[] = []
socket.onmessage = (e) => { buffer.push(JSON.parse(e.data)); schedule() }

function schedule() {
  requestAnimationFrame(() => {
    api.applyTransaction({
      add:    buffer.filter((d) => d.type === 'add').map((d) => d.row),
      update: buffer.filter((d) => d.type === 'update').map((d) => d.row),
      remove: buffer.filter((d) => d.type === 'remove').map((d) => d.id),
    })
    buffer = []
  })
}

What a transaction costs

update looks each row up in an id-to-index map built once per data array and carried across update-only transactions, so a batch of a thousand updates on a hundred-thousand-row grid does a thousand lookups and one copy of the array, not a hundred thousand getRowId calls. add and remove change indices, so a transaction with either leaves the next array to build its map on first use.

The array the transaction produces then goes through the row model, which reuses every row object the transaction did not replace and, for an update-only transaction on a sorted grid, repairs the sort around the replaced rows instead of re-sorting everything. What that costs, and the conditions under which the full pipeline runs instead, are on the benchmarks page and in real-time updates.

Notes

See the live Transaction API demo.

Add, update and remove in one pass

applyTransaction takes the three operations together and applies them once, so the grid re-renders a single time rather than three. update and remove match on getRowId, which is why a stable id is the prerequisite.

<script lang="ts">
  import { SvGrid, type GridColumns, type SvGridApi } from '@svgrid/grid'

  type Person = {
    id: number
    name: string
    department: string
    city: string
    salary: number
    bio: string
  }

  const seed: Person[] = [
    { id: 1, name: 'Ada Lovelace',   department: 'Engineering', city: 'London',   salary: 142000,
      bio: 'Wrote the first algorithm intended for a machine, and the first account of what a general-purpose computer could do beyond arithmetic.' },
    { id: 2, name: 'Grace Hopper',   department: 'Engineering', city: 'New York', salary: 168000,
      bio: 'Built the first compiler and argued that programs should be written in something closer to English than to machine code.' },
    { id: 3, name: 'Linus Torvalds', department: 'Platform',    city: 'Portland', salary: 155000,
      bio: 'Wrote a kernel and, later, the version control system that most of the industry now runs on.' },
  ]

  let rows = $state<Person[]>(seed.map((p) => ({ ...p })))
  let api = $state<SvGridApi<{}, Person> | null>(null)
  let result = $state('(no transaction yet)')

  const columns: GridColumns<Person> = [
    { field: 'name',   header: 'Name',   width: 180 },
    { field: 'city',   header: 'City',   width: 140 },
    { field: 'salary', header: 'Salary', width: 140,
      format: { type: 'currency', currency: 'USD' } },
  ]

  function run() {
    const out = api?.applyTransaction({
      add: [{ id: 4, name: 'Radia Perlman', department: 'Networking', city: 'Seattle', salary: 161000, bio: 'Designed the spanning tree protocol.' }],
      update: [{ ...rows[0]!, city: 'Cambridge' }],
      remove: ['3'],
    })
    result = JSON.stringify(out)
  }
</script>

<button type="button" onclick={run}>Add one, move Ada, drop Linus</button>

<SvGrid data={rows} {columns} getRowId={(r) => String(r.id)} onApiReady={(next) => (api = next)} />

<p>Result: <code>{result}</code></p>

Why the id matters

Without getRowId the grid falls back to array position, and a transaction that removes row 1 renumbers everything after it. Ids are what make "update the row I mean" survive a sort or a concurrent insert.

<script lang="ts">
  import { SvGrid, type GridColumns, type SvGridApi } from '@svgrid/grid'

  type Person = {
    id: number
    name: string
    department: string
    city: string
    salary: number
    bio: string
  }

  const seed: Person[] = [
    { id: 1, name: 'Ada Lovelace',   department: 'Engineering', city: 'London',   salary: 142000,
      bio: 'Wrote the first algorithm intended for a machine, and the first account of what a general-purpose computer could do beyond arithmetic.' },
    { id: 2, name: 'Grace Hopper',   department: 'Engineering', city: 'New York', salary: 168000,
      bio: 'Built the first compiler and argued that programs should be written in something closer to English than to machine code.' },
    { id: 3, name: 'Linus Torvalds', department: 'Platform',    city: 'Portland', salary: 155000,
      bio: 'Wrote a kernel and, later, the version control system that most of the industry now runs on.' },
  ]

  let rows = $state<Person[]>(seed.map((p) => ({ ...p })))
  let api = $state<SvGridApi<{}, Person> | null>(null)

  const columns: GridColumns<Person> = [
    { field: 'id',   header: 'ID',   width: 80 },
    { field: 'name', header: 'Name', width: 190 },
    { field: 'city', header: 'City', width: 150 },
  ]
</script>

<div>
  <button type="button" onclick={() => api?.applyTransaction({ remove: ['2'] })}>
    Remove id 2
  </button>
  <button type="button" onclick={() => api?.applyTransaction({ update: [{ ...rows[0]!, city: 'Oxford' }] })}>
    Update id 1
  </button>
</div>

<SvGrid data={rows} {columns} getRowId={(r) => String(r.id)} sortable onApiReady={(next) => (api = next)} />