Data binding & state management basics
The declarative model
React Spreadsheet follows a declarative approach: you store sheet data in React state, pass it as props, and the wrapper automatically diffs your data against the current widget state, applying only the changes.
import { useState } from "react";
import { ReactSpreadsheet, type SheetData } from "@dhtmlx/trial-react-spreadsheet";
import "@dhtmlx/trial-react-spreadsheet/spreadsheet.react.css";
function App() {
const [sheets, setSheets] = useState<SheetData[]>([
{
id: "sheet1",
name: "Data",
cells: {
A1: { value: "Hello" },
},
},
]);
return <ReactSpreadsheet sheets={sheets} activeSheet="sheet1" />;
}
Updating cells
Use immutable state updates with the functional setState updater:
const updateCell = (sheetId: string, cell: string, value: string | number) => {
setSheets((prev) =>
prev.map((sheet) =>
sheet.id === sheetId
? {
...sheet,
cells: {
...sheet.cells,
[cell]: { ...sheet.cells[cell], value },
},
}
: sheet
)
);
};
Responding to user actions
Use onAfterAction to react to user changes. Combine it with ref to read the current widget data:
import { useRef } from "react";
import { ReactSpreadsheet, type SpreadsheetRef } from "@dhtmlx/trial-react-spreadsheet";
function App() {
const ref = useRef<SpreadsheetRef>(null);
const [sheets, setSheets] = useState<SheetData[]>([/* ... */]);
const handleAfterAction = () => {
const data = ref.current?.instance?.serialize();
if (data) {
// Sync widget state back to React state
console.log("Spreadsheet data:", data);
}
};
return (
<ReactSpreadsheet
ref={ref}
sheets={sheets}
onAfterAction={handleAfterAction}
/>
);
}
The ref escape hatch
For operations that don't map to declarative props, use the SpreadsheetRef to access the underlying widget instance:
- Serialize data:
ref.current?.instance?.serialize() - Undo/redo:
ref.current?.instance?.undo()/ref.current?.instance?.redo() - Get cell value:
ref.current?.instance?.getValue("A1") - Programmatic selection:
ref.current?.instance?.selection.setSelectedCell("A1:C5")
const ref = useRef<SpreadsheetRef>(null);
const handleExport = () => {
const data = ref.current?.instance?.serialize();
console.log(data);
};
<ReactSpreadsheet ref={ref} sheets={sheets} />
Avoid mixing imperative writes (e.g. instance.setValue()) with the declarative sheets prop. The wrapper may overwrite imperative changes on the next render cycle. Use the ref only for reading data and for operations like undo/redo, selection, and export.
Controlled search
Use the search prop with onSearchResults for controlled search:
const [search, setSearch] = useState<SearchConfig | undefined>();
const [results, setResults] = useState<string[]>([]);
<input
placeholder="Search..."
onChange={(e) =/> setSearch({ query: e.target.value, open: true })}
/>
<p>{results.length} cells found</p>
<ReactSpreadsheet
sheets={sheets}
search={search}
onSearchResults={setResults}
/>
Undo / redo
Use onStateChange to track undo/redo availability, and call undo()/redo() via ref:
const ref = useRef<SpreadsheetRef>(null);
const [history, setHistory] = useState({ canUndo: false, canRedo: false });
<button
disabled={!history.canUndo}
onClick={() => ref.current?.instance?.undo()}
>
Undo
</button>
<button
disabled={!history.canRedo}
onClick={() => ref.current?.instance?.redo()}
>
Redo
</button>
<ReactSpreadsheet
ref={ref}
sheets={sheets}
onStateChange={setHistory}
/>
Performance
- Use
useMemofor derived sheets to avoid unnecessary recalculations:
const filteredSheets = useMemo(
() => sheets.filter((s) => s.name !== "Hidden"),
[sheets]
);
<ReactSpreadsheet sheets={filteredSheets} />
- Avoid recreating the
stylesobject on every render. Define it outside the component or wrap it inuseMemo. - Use the functional
setStateupdater to avoid stale closure issues in event callbacks.