Migrating from v2 to v3
This guide covers the breaking changes and migration steps needed when upgrading from GridSheet v2 to v3.
Overview of Changes
v3.0.0 introduces async/await support for formulas and several breaking changes. The good news is that most of these changes are straightforward to adopt.
Breaking Changes
1. CellType.system Property Removed
In v2, system metadata was stored directly on the cell object. In v3, it is no longer available as a cell property.
From v2:
const cell = table.getCellByPoint({ x: 0, y: 0 });
if (cell?.system?.changedAt) {
console.log('Last changed at:', cell.system.changedAt);
}To v3:
System metadata is now accessed via getSystem() on the Sheet class (an internal API). Because callbacks and sheetRef expose the UserSheet interface — which does not include getSystem() — you must go through sheet.__raw__ to reach it:
import { Sheet } from '@gridsheet/react-core';
const book = useBook({
onChange: ({ sheet }) => {
const raw: Sheet = sheet.__raw__;
const system = raw.getSystem({ y: 0, x: 0 });
if (system?.changedTime) {
console.log('Last changed at:', system.changedTime);
}
},
});Note:
sheet.__raw__exposes the internalSheetclass, which is not part of the public API and may change in future versions. Use it only when noUserSheetalternative is available.
What Changed:
cell.system(andcell._sys) are no longer available onCellType- System metadata is retrieved via
getSystem(point)on the internalSheetclass, accessed throughsheet.__raw__ changedAthas been renamed tochangedTimeinside theSystemtypechangedTimeis consistent with the sheet-levelsheet.changedTimeandsheet.lastChangedTimeproperties
UserSheet vs Sheet
v3 formalizes the distinction between UserSheet and Sheet:
UserSheetis the public interface exposed to application code. All callbacks (onChange,onInit, etc.) andsheetRefprovide aUserSheet. Rely on this for stable, version-safe code.Sheetis the concrete class that implementsUserSheet. It contains additional internal methods not included inUserSheet. Access it viasheet.__raw__when absolutely necessary.
See the Sheet API Reference for details.
2. options.showAddress Removed
The showAddress option that overlaid cell addresses in the top-right corner of each cell has been removed from OptionsType.
From v2:
<GridSheet
initialCells={initialCells}
options={{ showAddress: true }}
/>To v3:
Implement the same visual using a Policy with renderCallback. Use p2a(point) to convert the {y, x} coordinates to an address string, then wrap the rendered content:
import {
GridSheet,
Policy,
PolicyMixinType,
RenderProps,
useBook,
p2a,
buildInitialCells,
} from '@gridsheet/react-core';
const AddressOverlayMixin: PolicyMixinType = {
renderCallback(rendered: any, { point }: RenderProps) {
return (
<div style={{ position: 'relative', width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<span style={{ position: 'absolute', top: 2, right: 4, fontSize: '9px', color: '#aaa', fontFamily: 'monospace', pointerEvents: 'none', userSelect: 'none' }}>
{p2a(point)}
</span>
{rendered}
</div>
);
},
};
function MySheet() {
const book = useBook({
policies: { address: new Policy({ mixins: [AddressOverlayMixin] }) },
});
return (
<GridSheet
book={book}
initialCells={buildInitialCells({ cells: { default: { policy: 'address' } } })}
/>
);
}See Case 2 for a full working example.
3. Connector Split into SheetHandle and StoreHandle
In v2, a single Connector object was used to link a GridSheet to external code. In v3 it has been replaced by two focused handles: SheetHandle and StoreHandle.
| Concern | Old (Connector) | New |
|---|---|---|
| Sheet data access | connector.current.tableManager.instance (UserTable) | sheetRef.current?.sheet (UserSheet) |
| Push sheet update | tableManager.sync(table) | sheetRef.current?.apply(sheet) |
| UI state access | connector.current.storeManager.instance | storeRef.current?.store |
| Push store update | storeManager.sync | storeRef.current?.apply(store) |
| Raw dispatch | — | storeRef.current?.dispatch |
| Prop name | connector={connector} | sheetRef={sheetRef} storeRef={storeRef} |
From v2:
import { useConnector } from '@gridsheet/react-core';
const connector = useConnector();
// read sheet data
const table = connector.current.tableManager.instance;
return <GridSheet connector={connector} initialCells={initialCells} />;To v3:
import {
GridSheet,
useSheetRef,
useStoreRef,
} from '@gridsheet/react-core';
const sheetRef = useSheetRef();
const storeRef = useStoreRef();
// read sheet data
const sheet = sheetRef.current?.sheet;
// programmatically move the cursor
const store = storeRef.current?.store;
storeRef.current?.apply({ ...store, choosing: { y: 2, x: 3 } });
return (
<GridSheet
sheetRef={sheetRef}
storeRef={storeRef}
initialCells={initialCells}
/>
);When working outside a React component, use the createSheetRef / createStoreRef factory functions instead of the use* hooks.
What Changed:
useConnector()andcreateConnector()are removed; replace them withuseSheetRef()/createSheetRef()anduseStoreRef()/createStoreRef()- The data type for sheet content changed from
UserTabletoUserSheet SheetHandleexposes sheet data and anapplymethod to push updatesStoreHandleexposes UI state (cursor, selection mode, dimensions, etc.) and both anapplymethod and a rawdispatchfunction
4. BaseFunction Structure Changes
The BaseFunction base class for custom formula functions has been reworked. The helpTexts array is gone; documentation is now declared with a single description string on the class and an inline description on each argument definition.
From v2:
class MyFunc extends BaseFunction {
helpTexts = ['Computes something.', 'arg1: the input value'];
protected main(arg1: string) {
return arg1.toUpperCase();
}
}To v3:
import {
BaseFunction,
FunctionArgumentDefinition,
FunctionCategory,
} from '@gridsheet/react-core';
class MyFunc extends BaseFunction {
example = 'MY_FUNC("hello")';
description = 'Computes something.';
category: FunctionCategory = 'text';
defs: FunctionArgumentDefinition[] = [
{
name: 'arg1',
description: 'The input value.',
acceptedTypes: ['string', 'number', 'boolean'],
},
];
protected main(arg1: string) {
return arg1.toUpperCase();
}
}Automatic Validation
A key improvement in v3 is that BaseFunction now automatically validates arguments before main() is called, based on the defs schema. You no longer need to write boilerplate argument-count or type checks inside main():
- Argument count — the minimum and maximum allowed counts are derived from
defs. If too few or too many arguments are passed, a#N/Aerror is thrown automatically. - Type checking — each argument is verified against its
acceptedTypeslist. A#VALUE!error is thrown on mismatch. - Variadic args — set
variadic: trueon a definition to accept a repeating parameter. - Optional args — set
optional: trueto allow the argument to be omitted. - Matrix extraction — by default, a 1×1 matrix is automatically unwrapped to a scalar before being passed to
main(). SettakesMatrix: trueto suppress this.
When the built-in validation is insufficient you can override validate(args) to apply custom coercion, then return the processed argument list:
protected validate(args: any[]): any[] {
const matrix = this.extractNumberMatrix(args[0], 'matrix');
this.requireSquare(matrix, 'MY_MATRIX_FUNC');
return [matrix];
}What Changed:
helpTexts: string[]is removed; usedescription: stringon the class anddescriptionon eachdefsentrydefs: FunctionArgumentDefinition[]drives automatic argument-count and type validation beforemain()is invokedexample: stringdocuments a canonical call expression shown in formula autocomplete
Providing Built-in Functions
If you want to use the standard suite of spreadsheet functions (like SUM, VLOOKUP, etc.), we highly recommend using the useSpellbook() hook from @gridsheet/react-core/spellbook to provide them to your book. This requires @gridsheet/functions to be installed:
import { useBook } from '@gridsheet/react-core';
import { useSpellbook } from '@gridsheet/react-core/spellbook';
function App() {
const spellbook = useSpellbook();
const book = useBook({
additionalFunctions: spellbook,
});
// ...
}5. Renderer, Parser, Labeler, and Policy Merged
In v2, Renderer, Parser, Labeler, and Policy were separate classes with separate mixin systems. In v3 they have been merged into a single Policy class.
From v2:
import { Renderer, Policy, Parser, Labeler } from '@gridsheet/react-core';
const myRenderer = new Renderer({
mixins: [ThousandSeparatorRendererMixin],
});
const myPolicy = new Policy({
mixins: [DropdownPolicyMixin],
});To v3:
import { Policy } from '@gridsheet/react-core';
// Rendering and policy behaviour are both configured through Policy
const myPolicy = new Policy({
mixins: [ThousandSeparatorPolicyMixin, DropdownPolicyMixin],
});PolicyMixinType now covers rendering, serialization, deserialization, scalar extraction, and selection helpers all in one place. The most commonly used mixin hooks are:
| Hook | Purpose |
|---|---|
render | Fully custom cell renderer |
renderCallback | Wrap the default rendered output |
renderRowHeaderLabel / renderColHeaderLabel | Custom header labels |
serialize / deserialize | Clipboard and init value conversion |
toScalar | How the cell value is passed to formula evaluation |
getSelectOptions | Dropdown/autocomplete choices |
What Changed:
Renderer,Parser, andLabelerclasses are removed; replace allnew Renderer(...)(and parser/labeler equivalents) withnew Policy(...)- All
*RendererMixin,*ParserMixin,*LabelerMixinobjects must be replaced with their*PolicyMixinequivalents renderer,parser,labelerprops and their corresponding options no longer exist; usepoliciesandpolicyon cells
6. Header-Only Cell Addressing with ch() and rh()
v3 introduces dedicated address keys specifically for targeting header cells without affecting the data cells below or beside them.
Internally, these address keys append or prepend learning a 0 (e.g., A0 for column A's header, 01 for row 1's header). However, we strongly recommend using the provided coordinate utility functions ch() and rh() to generate these addressing strings instead of writing the internal 0 variants manually.
Previous behavior (still valid for data cells):
| Key | Example | Applies to |
|---|---|---|
A | column letter | All data cells in column A (A1, A2...). Crucially, this does not include the column header (A0). Therefore, setting width here will not change the column's width in the UI. |
1 | row number | All data cells in row 1 (A1, B1...). This does not include the row header (01). Therefore, setting height here will not change the row's height in the UI. |
New header-only keys:
| Key | Example | Applies to |
|---|---|---|
ch('A') (evaluates to A0) | ch('A') | Only the column header cell for column A. You must define width here to resize the column visually. |
rh(1) (evaluates to 01) | rh(1) | Only the row header cell for row 1. You must define height here to resize the row visually. |
The critical difference is that header cells targeted via ch and rh fully support style and are the authoritative source for layout sizes (width and height). Note that A and 1 do not get copied to the headers.
import { buildInitialCells, ch, rh } from '@gridsheet/react-core';
buildInitialCells({
cells: {
// Style applied ONLY to the data cells in column A
A: { style: { color: 'green' } },
// Style, label, and width applied ONLY to the column-A header cell
[ch('A')]: {
width: 120,
label: 'Full Name',
style: { backgroundColor: '#e8f0fe', fontWeight: 'bold' },
},
// Style applied ONLY to the data cells in row 1
1: { style: { color: 'blue' } },
// Style and height applied ONLY to the row-1 header cell
[rh(1)]: {
height: 28,
label: 'R1',
style: { backgroundColor: '#fce8e6' },
},
},
});(removed outdated stacking order details as A and 1 do not propagate to header cells anymore)
What Changed / New:
Aand1strictly point to data cells (A1,A2... forAandA1,B1... for1) and do not cascade any properties to the header cells.- Because layout size is read from headers, setting
widthonAorheighton1no longer resizes the column/row in the UI. You must set them via the header address usingch()orrh(). ch('A')targets only the column header cell and fully supports bothwidthandstyle.rh(1)targets only the row header cell and fully supports bothheightandstyle.- Header cells now participate in the full style system; you can set background colour, font weight, borders, etc. directly on header cells.
Utility Functions for Header Address Keys
@gridsheet/react-core exports two helper functions for generating header address keys programmatically:
| Function | Signature | Example | Output |
|---|---|---|---|
ch | ch(col: string | number): string | ch('A') / ch(1) | 'A0' |
rh | rh(y: number): string | rh(1) / rh(10) | '01' / '010' |
import { ch, rh } from '@gridsheet/react-core';
ch('A') // → 'A0' (column header key for column A)
ch(1) // → 'A0' (also accepts a 1-based column index)
ch('Z') // → 'Z0'
rh(1) // → '01' (row header key for row 1)
rh(10) // → '010'Note:
ch(col)returns the header-only keyA0. Use a plain column letter (e.g.'A') when you want to setwidthacross all cells in the column. Usech(col)when you want to target the column header cell specifically (e.g. to set itsstyleorlabel).
These helpers are especially useful when building CellsByAddressType dynamically:
import { buildInitialCells, ch, rh } from '@gridsheet/react-core';
const columns = ['A', 'B', 'C'];
buildInitialCells({
cells: {
// Apply width, label, and style to each column header cell only (ch() key)
...Object.fromEntries(
columns.map((col, i) => [ch(col), { label: `Column ${i + 1}`, width: 120, style: { fontWeight: 'bold' } }])
),
// Set row height on each row header cell (rh() key)
...Object.fromEntries(
Array.from({ length: 10 }, (_, i) => [rh(i + 1), { height: 28 }])
),
},
});7. Sheet Methods
Several sheet methods have been refactored to support async operations. Review the Sheet API Reference for the complete and updated method signatures.
8. Filter Operations
Filter operations now properly apply to all sheet operations (delete, copy, move, paste). If your code relies on filters being ignored during these operations, you may need to adjust your logic.
New Features to Adopt
Async Formulas
v3 introduces first-class support for async formulas. This is completely optional and backward compatible—all existing synchronous formulas continue to work.
To create an async formula, extend BaseFunctionAsync:
import { BaseFunctionAsync } from '@gridsheet/react-core';
class FetchWeather extends BaseFunctionAsync {
example = 'FETCH_WEATHER("Tokyo")';
description = 'Fetches weather data for a city.';
defs = [{ name: 'city', description: 'City name' }];
async main(city: string) {
const response = await fetch(`https://api.weather.example.com/city=${city}`);
const data = await response.json();
return data.temperature;
}
}Async formulas are automatically awaited during evaluation, and filtering/sorting operations automatically wait for all async computations to complete.
Sheet Data Extraction with toValue* / toCell* Functions
Instead of calling methods directly on a UserSheet object to read data, v3 provides a set of standalone utility functions. Import them alongside your other @gridsheet/react-core imports and pass the sheet received from a callback or sheetRef.
import {
toValueMatrix,
toValueObject,
toValueRows,
toValueCols,
toCellMatrix,
toCellObject,
toCellRows,
toCellCols,
} from '@gridsheet/react-core';Overview
| Function | Returns | Best for |
|---|---|---|
toValueMatrix | any[][] | Feeding data into a chart or table |
toValueObject | { [address]: any } | Keyed lookup by cell address |
toValueRows | { [col]: any }[] | Row-oriented export (one object per row) |
toValueCols | { [row]: any }[] | Column-oriented export (one object per column) |
toCellMatrix | (CellType | null)[][] | Reading styles/metadata alongside values |
toCellObject | { [address]: CellType } | Partial reads of specific cells with metadata |
toCellRows | { [col]: CellType }[] | Row-oriented read with full cell objects |
toCellCols | { [row]: CellType }[] | Column-oriented read with full cell objects |
All functions share these common options:
| Option | Type | Default | Description |
|---|---|---|---|
resolution | 'RESOLVED' | 'EVALUATED' | 'RAW' | 'SYSTEM' | 'RESOLVED' | How formula results are resolved |
raise | boolean | false | Throw on formula errors instead of returning an error string |
filter | CellFilter | (all cells) | Predicate to include or exclude cells |
asScalar | boolean | false | Coerce values through the cell's policy |
Examples
Read all values as a 2-D array (useful for chart libraries):
const book = useBook({
onChange: ({ sheet }) => {
const matrix = toValueMatrix(sheet);
// [[1, 2, 3], [4, 5, 6], ...]
},
});Read only the cells that changed in the last edit:
const book = useBook({
onChange: ({ sheet }) => {
const changed = toCellObject(sheet, {
addresses: sheet.getLastChangedAddresses(),
});
// { A1: CellType, B2: CellType, ... }
},
});Read values keyed by address, restricting to a rectangular area:
const matrix = toValueMatrix(sheet, {
area: { top: 1, left: 1, bottom: 5, right: 3 },
});Read unevaluated formula strings:
const raw = toValueMatrix(sheet, { resolution: 'RAW' });Row-oriented export for serialisation:
const rows = toValueRows(sheet);
console.log(rows[0]); // { A: 'Alice', B: 30, C: 'Engineer' }Using with sheetRef
const sheetRef = useSheetRef();
function handleExport() {
const sheet = sheetRef.current?.sheet;
if (!sheet) return;
const data = toValueObject(sheet);
console.log(data);
}
return <GridSheet sheetRef={sheetRef} initialCells={initialCells} />;Context Menus on Headers
v3 enhances the context menu system. You can now open menus on row headers and the top-left corner area. Check the API Reference for implementation details.
Migration Checklist
- Search codebase for
cell.system/cell._sysand replace withsheet.getSystem(point) - Replace
changedAtwithchangedTimein any system metadata access - Remove
options.showAddressand replace with arenderCallbackpolicy if needed (see Case 2) - Replace
useConnector()/createConnector()withuseSheetRef()/createSheetRef()anduseStoreRef()/createStoreRef() - Update connector prop usage:
connector={connector}→sheetRef={sheetRef} storeRef={storeRef} - Replace
UserTablereferences accessed via a connector withUserSheetviasheetRef - Replace
new Renderer(...)withnew Policy(...)and rename all*RendererMixinto*PolicyMixin - Remove
renderer/renderersprops and migrate topolicies+ per-cellpolicy - Replace
helpTextsin custom formula classes with adescriptionstring and per-argumentdescriptionindefs - Add
defsentries to custom formula classes to enable automatic argument validation - Review cell address keys: if you applied
styleon a column key (A) or row key (1) to style the header, move those styles toA0or01 - Review sheet method implementations and update signatures if needed
- Test filter/sort operations, especially with async data
- Consider implementing async formulas for external API integration
- Update unit tests to reflect the new
changedTimeproperty - Run full test suite to ensure compatibility
Need Help?
If you encounter issues during migration:
- Check the Sheet API Reference for updated method signatures
- Review case11 example for async formula patterns
- Refer to the Architecture Guide for technical details