Skip to content

Row data ​

Rows are passed as an array of objects with the rows prop:

vue
<Datagrid :columns="columns" :rows="rows" />
tsx
<Datagrid columns={columns} rows={rows} />

Row ids ​

Every row gets a unique id (uid). Without configuration the index of the row is used (0, 1, 2, … and "0-1" for child rows of tree data).

When rows are added, removed or reordered, index based ids change. Selection, focus, expanded state and row heights are bound to the id, so set rowId whenever the data can change:

ts
const options: GridOptions<Person> = {
  rowId: (data) => data.id,
};

The id is used by the selection models (v-model:selectedItems), v-model:expandedItems, the state and API methods like api.getRow(id).

Updating data ​

The grid watches the rows prop (not deeply). Assign a new array to update the grid. Thanks to rowId the selection survives adding, removing, replacing and reordering rows – select some rows and try the buttons:

Source
vue
<template>
  <div class="demo">
    <div class="demo-toolbar">
      <button @click="add">Add employee</button>
      <button @click="removeSelected">Remove selected</button>
      <button @click="raiseSalary">Raise salary of selected by 5 %</button>
      <button @click="rows = rows.toReversed()">Reverse order</button>
      <span>{{ rows.length }} rows, {{ selected.length }} selected</span>
    </div>
    <Datagrid :columns="columns" :rows="rows" :options="options" v-model:selectedItems="selected" />
  </div>
</template>

<script setup lang="ts">
import { ref, shallowRef } from "vue";
import { Datagrid, formatters, type ColumnConfig, type GridOptions, type ItemUid } from "@datagrid/vue-ui";
import { createEmployees, type Employee } from "./data";

const rows = shallowRef(createEmployees(8));
const selected = ref<Array<ItemUid>>([2, 5]);
let nextId = 9;

function add() {
  const employee = { ...createEmployees(1, nextId)[0], id: nextId++ };
  rows.value = [employee, ...rows.value];
}

function removeSelected() {
  rows.value = rows.value.filter((row) => !selected.value.includes(row.id));
}

function raiseSalary() {
  // replace the changed objects, the grid finds the rows by their id
  rows.value = rows.value.map((row) =>
    selected.value.includes(row.id) ? { ...row, salary: Math.round(row.salary * 1.05) } : row
  );
}

const columns: Array<ColumnConfig<Employee>> = [
  { field: "id", text: "ID", type: "number", width: 70 },
  { field: "firstName", text: "First name" },
  { field: "lastName", text: "Last name" },
  { field: "department", text: "Department" },
  { field: "salary", text: "Salary", type: "number", valueFormatter: formatters.currency("EUR", { maximumFractionDigits: 0 }) },
];

const options: GridOptions<Employee> = {
  // selection, focus and expanded state are bound to the id
  rowId: (data) => data.id,
  selection: "Row",
  singleSelect: false,
  checkboxSelection: true,
  defaultColumn: { flex: 1 },
};
</script>
tsx
import { useRef, useState } from "react";
import { Datagrid, formatters, type ColumnConfig, type GridOptions, type ItemUid } from "@datagrid/react-ui";
import { createEmployees, type Employee } from "../data";

const columns: Array<ColumnConfig<Employee>> = [
  { field: "id", text: "ID", type: "number", width: 70 },
  { field: "firstName", text: "First name" },
  { field: "lastName", text: "Last name" },
  { field: "department", text: "Department" },
  { field: "salary", text: "Salary", type: "number", valueFormatter: formatters.currency("EUR", { maximumFractionDigits: 0 }) },
];

const options: GridOptions<Employee> = {
  // selection, focus and expanded state are bound to the id
  rowId: (data) => data.id,
  selection: "Row",
  singleSelect: false,
  checkboxSelection: true,
  defaultColumn: { flex: 1 },
};

export default function DataUpdate() {
  const [rows, setRows] = useState(() => createEmployees(8));
  const [selected, setSelected] = useState<Array<ItemUid>>([2, 5]);
  const nextId = useRef(9);

  function add() {
    const employee = { ...createEmployees(1, nextId.current)[0], id: nextId.current++ };
    setRows([employee, ...rows]);
  }

  function removeSelected() {
    setRows(rows.filter((row) => !selected.includes(row.id)));
  }

  function raiseSalary() {
    // replace the changed objects, the grid finds the rows by their id
    setRows(rows.map((row) => (selected.includes(row.id) ? { ...row, salary: Math.round(row.salary * 1.05) } : row)));
  }

  return (
    <div className="demo">
      <div className="demo-toolbar">
        <button onClick={add}>Add employee</button>
        <button onClick={removeSelected}>Remove selected</button>
        <button onClick={raiseSalary}>Raise salary of selected by 5 %</button>
        <button onClick={() => setRows(rows.toReversed())}>Reverse order</button>
        <span>
          {rows.length} rows, {selected.length} selected
        </span>
      </div>
      <Datagrid columns={columns} rows={rows} options={options} selectedItems={selected} onSelectedItemsChange={setSelected} />
    </div>
  );
}
ts
const rows = ref<Array<Person>>([]);

rows.value = await loadPersons();          // replace all rows
rows.value = [...rows.value, newPerson];    // add a row
rows.value = rows.value.filter((p) => p.id !== id); // remove a row
tsx
const [rows, setRows] = useState<Array<Person>>([]);

setRows(await loadPersons());                   // replace all rows
setRows([...rows, newPerson]);                  // add a row
setRows(rows.filter((p) => p.id !== id));       // remove a row

Existing row objects are reused (by rowId), so updates are fast even for large data sets. Sorting, filtering and grouping are applied again after each update.

Mutating row objects ​

If you change properties of row objects directly (without assigning a new array), call refresh() to apply sorting and filtering again and to rerender the cells. This avoids copying objects, e.g. for frequent updates of single values. Here the list is sorted by stock: selling units of the selected product moves it up, cells with flashOnChange are highlighted.

Source
vue
<template>
  <div class="demo">
    <div class="demo-toolbar">
      <button @click="sell">Sell 10 units of the selected product</button>
      <button @click="restock">Restock products below 20 units</button>
    </div>
    <Datagrid
      :columns="columns"
      :rows="products"
      :options="options"
      v-model:selectedItems="selected"
      @ready="api = $event"
    />
  </div>
</template>

<script setup lang="ts">
import { ref, shallowRef } from "vue";
import { Datagrid, formatters, type ColumnConfig, type Core, type GridOptions, type ItemUid } from "@datagrid/vue-ui";
import { createProducts, type Product } from "./data";

// a plain (not reactive) array, the objects are changed directly
const products = createProducts(40);
const api = shallowRef<Core>();
const selected = ref<Array<ItemUid>>([25]);

function sell() {
  const product = products.find((p) => p.id === selected.value[0]);
  if (!product) return;
  product.stock = Math.max(0, product.stock - 10);
  // sorting and filters are applied again, the cells are rendered again
  api.value?.refresh();
}

function restock() {
  products.filter((p) => p.stock < 20).forEach((p) => (p.stock += 50));
  api.value?.refresh();
}

const columns: Array<ColumnConfig<Product>> = [
  { field: "name", text: "Product", flex: 1 },
  { field: "category", text: "Category" },
  { field: "price", text: "Price", type: "number", valueFormatter: formatters.currency("EUR") },
  {
    field: "stock",
    text: "Stock",
    type: "number",
    sort: "asc",
    flashOnChange: true,
    cellClass: ({ value }) => (value < 20 ? "stock-low" : undefined),
  },
];

const options: GridOptions<Product> = {
  rowId: (data) => data.id,
  selection: "Row",
  defaultColumn: { sortable: true, width: 130 },
};
</script>

<style>
.stock-low {
  color: #d03030;
  font-weight: 600;
}
</style>
tsx
import { useRef, useState } from "react";
import { Datagrid, formatters, type ColumnConfig, type DatagridHandle, type GridOptions, type ItemUid } from "@datagrid/react-ui";
import { createProducts, type Product } from "../data";

const columns: Array<ColumnConfig<Product>> = [
  { field: "name", text: "Product", flex: 1 },
  { field: "category", text: "Category" },
  { field: "price", text: "Price", type: "number", valueFormatter: formatters.currency("EUR") },
  {
    field: "stock",
    text: "Stock",
    type: "number",
    sort: "asc",
    flashOnChange: true,
    cellClass: ({ value }) => (value < 20 ? "stock-low" : undefined),
  },
];

const options: GridOptions<Product> = {
  rowId: (data) => data.id,
  selection: "Row",
  defaultColumn: { sortable: true, width: 130 },
};

export default function DataMutate() {
  // the array is never replaced, the objects are changed directly
  const [products] = useState(() => createProducts(40));
  const grid = useRef<DatagridHandle>(null);
  const [selected, setSelected] = useState<Array<ItemUid>>([25]);

  function sell() {
    const product = products.find((p) => p.id === selected[0]);
    if (!product) return;
    product.stock = Math.max(0, product.stock - 10);
    // sorting and filters are applied again, the cells are rendered again
    grid.current?.refresh();
  }

  function restock() {
    products.filter((p) => p.stock < 20).forEach((p) => (p.stock += 50));
    grid.current?.refresh();
  }

  return (
    <div className="demo">
      <div className="demo-toolbar">
        <button onClick={sell}>Sell 10 units of the selected product</button>
        <button onClick={restock}>Restock products below 20 units</button>
      </div>
      <Datagrid
        ref={grid}
        columns={columns}
        rows={products}
        options={options}
        selectedItems={selected}
        onSelectedItemsChange={setSelected}
      />
      <style>{`
        .stock-low {
          color: #d03030;
          font-weight: 600;
        }
      `}</style>
    </div>
  );
}
ts
product.stock -= 10;
api.value.refresh(); // api from the ready event
tsx
product.stock -= 10;
grid.current.refresh();

Changes made by the grid ​

When the user edits a cell or pastes values, the grid changes the row objects directly and reports a new array, so you can keep your data in sync:

vue
<Datagrid v-model:rows="rows" :columns="columns" />
tsx
const [rows, setRows] = useState(initialRows);

<Datagrid rows={rows} onRowsChange={setRows} columns={columns} />

Loading state ​

While rows is undefined or null, the grid shows the loading indicator. An empty array shows the "no rows" message. To keep the current rows visible while new data is loaded (e.g. a reload), set the option loading: true: the grid shows a progress bar above the rows.

Source
vue
<template>
  <div class="demo">
    <div class="demo-toolbar">
      <button @click="load(50)">Load</button>
      <button @click="load(0)">Load empty result</button>
      <button @click="reload">Reload (keep rows)</button>
    </div>
    <Datagrid :columns="columns" :rows="rows" :options="options" />
  </div>
</template>

<script setup lang="ts">
import { computed, onMounted, ref, shallowRef } from "vue";
import { Datagrid, type ColumnConfig, type GridOptions } from "@datagrid/vue-ui";
import { createEmployees, type Employee } from "./data";

// simulates a request to the server
const fetchEmployees = (count: number) =>
  new Promise<Array<Employee>>((resolve) => setTimeout(() => resolve(createEmployees(count, Date.now())), 1500));

const rows = shallowRef<Array<Employee>>();
const reloading = ref(false);

async function load(count: number) {
  // undefined shows the loading overlay instead of the rows
  rows.value = undefined;
  rows.value = await fetchEmployees(count);
}

async function reload() {
  // the loading option shows a progress bar and keeps the current rows
  reloading.value = true;
  rows.value = await fetchEmployees(50);
  reloading.value = false;
}

onMounted(() => load(50));

const columns: Array<ColumnConfig<Employee>> = [
  { field: "firstName", text: "First name" },
  { field: "lastName", text: "Last name" },
  { field: "department", text: "Department" },
  { field: "city", text: "City" },
];

const options = computed<GridOptions<Employee>>(() => ({
  rowId: (data) => data.id,
  loading: reloading.value,
  defaultColumn: { flex: 1 },
}));
</script>
tsx
import { useEffect, useMemo, useState } from "react";
import { Datagrid, type ColumnConfig, type GridOptions } from "@datagrid/react-ui";
import { createEmployees, type Employee } from "../data";

// simulates a request to the server
const fetchEmployees = (count: number) =>
  new Promise<Array<Employee>>((resolve) => setTimeout(() => resolve(createEmployees(count, Date.now())), 1500));

const columns: Array<ColumnConfig<Employee>> = [
  { field: "firstName", text: "First name" },
  { field: "lastName", text: "Last name" },
  { field: "department", text: "Department" },
  { field: "city", text: "City" },
];

export default function DataLoading() {
  const [rows, setRows] = useState<Array<Employee>>();
  const [reloading, setReloading] = useState(false);

  async function load(count: number) {
    // undefined shows the loading overlay instead of the rows
    setRows(undefined);
    setRows(await fetchEmployees(count));
  }

  async function reload() {
    // the loading option shows a progress bar and keeps the current rows
    setReloading(true);
    setRows(await fetchEmployees(50));
    setReloading(false);
  }

  useEffect(() => {
    load(50);
  }, []);

  const options = useMemo<GridOptions<Employee>>(
    () => ({
      rowId: (data) => data.id,
      loading: reloading,
      defaultColumn: { flex: 1 },
    }),
    [reloading]
  );

  return (
    <div className="demo">
      <div className="demo-toolbar">
        <button onClick={() => load(50)}>Load</button>
        <button onClick={() => load(0)}>Load empty result</button>
        <button onClick={reload}>Reload (keep rows)</button>
      </div>
      <Datagrid columns={columns} rows={rows} options={options} />
    </div>
  );
}

The texts can be replaced (Vue: loading and empty slots, React: renderLoading and renderEmpty), see Custom rendering.

Large data sets ​

The grid renders only the visible rows and columns, so the number of rows mainly affects sorting, filtering and the creation of the data. Tips for large data sets:

  • set rowId, updates of the data reuse existing row objects
  • use a fixed rowHeight, rows don't have to be measured
  • avoid expensive valueGetter functions for sorted or filtered columns

Vue makes every object inside of a ref deeply reactive. For large arrays this costs memory and time. The grid does not need reactive rows, so use shallowRef:

ts
const rows = shallowRef(hugeArray);
Source
vue
<template>
  <div class="demo">
    <div class="demo-toolbar">
      <label>
        Rows
        <select v-model.number="count">
          <option :value="1000">1,000</option>
          <option :value="100000">100,000</option>
          <option :value="500000">500,000</option>
        </select>
      </label>
      <button @click="generate">Generate</button>
      <span>{{ info }}</span>
    </div>
    <Datagrid :columns="columns" :rows="rows" :options="options" />
  </div>
</template>

<script setup lang="ts">
import { nextTick, ref, shallowRef } from "vue";
import { Datagrid, type ColumnConfig, type GridOptions } from "@datagrid/vue-ui";
import { createEmployees, type Employee } from "./data";

const count = ref(100000);
const info = ref("");
// shallowRef: Vue does not make the objects reactive (much faster, less memory)
const rows = shallowRef<Array<Employee>>([]);

async function generate() {
  const start = performance.now();
  const data = createEmployees(count.value, Math.round(Math.random() * 1000));
  const created = performance.now();
  rows.value = data;
  await nextTick();
  const end = performance.now();
  info.value = `created in ${Math.round(created - start)} ms, displayed in ${Math.round(end - created)} ms`;
}

generate();

const columns: Array<ColumnConfig<Employee>> = [
  { field: "id", text: "ID", type: "number", width: 90 },
  { field: "firstName", text: "First name" },
  { field: "lastName", text: "Last name" },
  { field: "department", text: "Department" },
  { field: "city", text: "City" },
  { field: "salary", text: "Salary", type: "number" },
  { field: "startDate", text: "Start date", type: "date" },
];

const options: GridOptions<Employee> = {
  rowId: (data) => data.id,
  // a fixed row height avoids measuring rows
  rowHeight: 32,
  defaultColumn: { sortable: true, filterable: true, flex: 1 },
};
</script>
tsx
import { useEffect, useRef, useState } from "react";
import { Datagrid, type ColumnConfig, type GridOptions } from "@datagrid/react-ui";
import { createEmployees, type Employee } from "../data";

const columns: Array<ColumnConfig<Employee>> = [
  { field: "id", text: "ID", type: "number", width: 90 },
  { field: "firstName", text: "First name" },
  { field: "lastName", text: "Last name" },
  { field: "department", text: "Department" },
  { field: "city", text: "City" },
  { field: "salary", text: "Salary", type: "number" },
  { field: "startDate", text: "Start date", type: "date" },
];

const options: GridOptions<Employee> = {
  rowId: (data) => data.id,
  // a fixed row height avoids measuring rows
  rowHeight: 32,
  defaultColumn: { sortable: true, filterable: true, flex: 1 },
};

export default function DataLarge() {
  const [count, setCount] = useState(100000);
  const [rows, setRows] = useState<Array<Employee>>([]);
  const [info, setInfo] = useState("");
  const created = useRef({ start: 0, end: 0 });

  function generate() {
    const start = performance.now();
    const data = createEmployees(count, Math.round(Math.random() * 1000));
    created.current = { start, end: performance.now() };
    setRows(data);
  }

  useEffect(generate, []);

  // runs after the grid has received the new rows
  useEffect(() => {
    const { start, end } = created.current;
    if (start) setInfo(`created in ${Math.round(end - start)} ms, displayed in ${Math.round(performance.now() - end)} ms`);
  }, [rows]);

  return (
    <div className="demo">
      <div className="demo-toolbar">
        <label>
          Rows{" "}
          <select value={count} onChange={(e) => setCount(Number(e.target.value))}>
            <option value={1000}>1,000</option>
            <option value={100000}>100,000</option>
            <option value={500000}>500,000</option>
          </select>
        </label>
        <button onClick={generate}>Generate</button>
        <span>{info}</span>
      </div>
      <Datagrid columns={columns} rows={rows} options={options} />
    </div>
  );
}

See Performance for more details.

Reading data ​

ts
api.getData();              // the rows array passed to the grid
api.getAllRows();           // all row objects (including child rows)
api.getAllVisibleRows();    // rows in the view (sorted, filtered, current page, without collapsed children)
api.getFilteredSortedItems(); // all rows after filtering and sorting (all pages)
api.getRow(id);             // row by id
api.getSelectedData();      // data objects of the selected rows
api.getTotalItemCount();    // number of rows
api.getFilteredItemCount(); // number of rows after filtering

A row object (Row<T>) contains the data object (row.data), the id (row.uid), the position in the view (row.index) and its state (expanded, showDetails, lvl).

Released under the ISC License.