Understanding data and indexes
Handsontable keeps two views of your data — the source data you provided and the visual dataset the user currently sees — and addresses each one with its own kind of index. Read this to understand which API methods operate on which view, and how features such as sorting, moving, hiding, and trimming affect them.
Background
When you pass a dataset to Handsontable, the grid doesn’t just display it — it keeps two related views of it:
- The source data is the dataset you provided, in the order you provided it. It’s what you get back from
getSourceData()and related methods, and it’s what you should persist. - The visual dataset is what the user currently sees, after any sorting, moving, hiding, or trimming has been applied. It’s what you get back from
getData()and related methods.
Most of the time, these two views show the same rows in the same order, so the distinction doesn’t matter. It starts to matter as soon as a feature reorders or removes rows or columns for display purposes only, because at that point getData() and getSourceData() start to disagree.
How it works
Source data vs. visual data
Take a 4-row source dataset and sort it by the first column. The source array itself doesn’t change — Handsontable stores the new order separately — so reading the two views gives different results:
// source data, as provided[ ['Ford', 2018], ['Audi', 2020], ['BMW', 2019], ['Toyota', 2021],]
hot.getSourceData();// -> the original 4 rows, in the original order
hot.getData();// -> the 4 rows sorted by year: Ford, BMW, Audi, ToyotaUse getSourceData() when you need the dataset as you provided it. Use getData() when you need what the user is currently looking at. The same split applies to the single-cell variants: getDataAtCell()/setDataAtCell() operate on the visual dataset, while getSourceDataAtCell()/setSourceDataAtCell() operate on the source data. For the full method list, see Binding to data: Related API reference.
Physical vs. visual indexes
Handsontable addresses these two views with two different kinds of row/column index:
- A physical index is a position in the source data array. It doesn’t change when you sort, move, hide, or trim — only when you insert or remove a row or column.
- A visual index is a position in what the user sees. It changes whenever sorting, moving, hiding, or trimming changes the display order.
| Takes | Use for | |
|---|---|---|
setDataAtCell(), getDataAtCell(), getDataAtRow(), getDataAtRowProp() | Visual indexes | Reading or writing what the user sees |
setSourceDataAtCell(), getSourceDataAtCell(), getSourceDataAtRow() | Physical indexes | Reading or writing the underlying source data |
The cells configuration function | Physical row and column | Per-cell settings keyed to the source data |
The cell configuration array | Visual row and column | Per-cell settings keyed to the displayed position |
To translate between the two, use toPhysicalRow(), toVisualRow(), toPhysicalColumn(), and toVisualColumn(). For example, after moving the row at visual position 0 to visual position 2:
| Visual position | Physical row |
|---|---|
| 0 | 1 |
| 1 | 2 |
| 2 | 0 |
hot.toPhysicalRow(0); // -> 1, the row now shown first was originally row 1hot.toVisualRow(0); // -> 2, the row originally first is now shown thirdSee Row moving: Set a pre-defined row order and Column moving: Set a pre-defined column order for worked examples of this mapping. Internally, Handsontable also tracks a third, renderable index for DOM rendering, but public API methods don’t take renderable indexes.
How features affect the two datasets
Different features remove or reorder rows and columns at different points between the source data and the DOM. This determines whether getData() still includes them:
| Hidden rows/columns | Trimmed rows | |
|---|---|---|
In getSourceData() | Yes | Yes |
In getData() (visual dataset) | Yes | No |
| Shifts other rows’/columns’ visual indexes | No | Yes |
| Rendered in the DOM | No | No |
Hiding removes a row or column only from rendering — it stays in the visual dataset and keeps its visual index. Trimming removes it from the visual dataset entirely, so every row or column after it shifts to a lower visual index. See Row hiding and Row trimming for the plugin-level details.
Every accepted data change — even a single cell — makes Handsontable re-render all visible cells. When you apply many changes at once, wrap them in batch() so the grid renders only once.
Trade-offs
Operate on the source data when you’re persisting to a backend, exporting, or otherwise working with the dataset independent of how it’s currently displayed. Operate on the visual dataset when you’re reacting to what the user sees or clicked, such as inside a selection or change hook.
Handsontable binds to your source data by reference rather than copying it, so it’s possible to read or write that array directly instead of going through the API. Avoid this: direct writes bypass hooks, validators, and index mapping, so the grid’s visual dataset can drift out of sync with the array. See Binding to data: Understand binding as a reference for the recommended alternative.
Related
- Binding to data — load data into the grid and choose between data structures.
- Saving data — persist changes back to a backend.
- Events and hooks — react to data changes, including the
sourceargument. - Row hiding and Row trimming — feature-level details for each.
- Row moving and Column moving — reorder the visual dataset without touching the source array.
getData(),getSourceData(),toPhysicalRow(),toVisualRow()API reference.