Examples
Case 5: A simplified menu with Multiple Sheets

A simplified menu with Multiple Sheets

This demo showcases a simplified menu bar and multiple sheet tabs.

It demonstrates how to create a professional spreadsheet application with localStorage persistence and color formatting.

The demo includes multiple sheets with different data types and a streamlined menu system.

Implementation Guide

📄 View Source Code

One useSheetRef / useStoreRef Per Sheet

Because React disallows conditional hook calls, you must declare a fixed set of refs at the component's top level — one pair per possible sheet. Map them by sheet name for easy lookup.

const salesSheetRef   = useSheetRef();
const salesStoreRef   = useStoreRef();
const budgetSheetRef  = useSheetRef();
const budgetStoreRef  = useStoreRef();
// ...
 
const sheetRefs = { Sales: salesSheetRef, Budget: budgetSheetRef, ... };
const storeRefs = { Sales: salesStoreRef, Budget: budgetStoreRef, ... };

When an action targets the active sheet, look up the correct ref by name:

const storeHandle = storeRefs[activeSheet]?.current;

Dispatching userActions for Undo / Redo / Cut / Copy / Paste

userActions exports a set of action creators that can be dispatched directly to the store.

import { userActions, clip, areaToZone } from '@gridsheet/react-core';
 
// Undo / Redo
storeHandle.dispatch(userActions.undo(null));
storeHandle.dispatch(userActions.redo(null));
 
// Cut requires the current selection area
const area = clip(store);
storeHandle.dispatch(userActions.cut(areaToZone(area)));
 
// Copy
storeHandle.dispatch(userActions.copy(areaToZone(area)));
 
// Paste (values only)
storeHandle.dispatch(userActions.paste({ matrix: [], onlyValue: true }));

clip(store) returns the selected area; areaToZone converts it to the zone format expected by cut/copy actions.

Auto-save on Ctrl+S via onSave

useSpellbook's onSave callback is triggered automatically when the user presses Ctrl+S inside the grid. Wire it to your save logic to provide Excel-like keyboard saving.

const book = useSpellbook({
  onSave: () => handleMenuAction('save'),
});

The save action reads sheetRef.current.sheet, serializes each cell's value and style into a matrix, and writes it to localStorage under a per-sheet key (gridsheet_demo5.<sheetName>).

Per-sheet localStorage Keys

Each sheet is stored independently under a namespaced key so sheets don't overwrite each other.

// Save
localStorage.setItem(`gridsheet_demo5.${activeSheet}`, JSON.stringify(saveData));
 
// Load recent
const allKeys = Object.keys(localStorage).filter((key) => key.startsWith('gridsheet_demo5.'));

On mount, the most recently saved entry (determined by timestamp) is used to restore sheets and activeSheet state.

Memoizing initialCells with useMemo

Computing buildInitialCells for all sheets on every render is expensive. Wrap the computation in useMemo keyed on the sheets array to recalculate only when sheets are added or removed.

const initialCellsForSheets = React.useMemo(() => {
  const cellsMap: Record<string, ReturnType<typeof buildInitialCells>> = {};
  allPossibleSheets.forEach((sheetName) => {
    // ... load from localStorage or fall back to SHEET_DATA
    cellsMap[sheetName] = buildInitialCells({ matrices: { A1: data }, cells: { ... } });
  });
  return cellsMap;
}, [sheets]);

All 10 possible sheet slots are always computed (to keep hook counts stable), but only the active sheet's result is actually rendered.

Adding a New Sheet at Runtime

To add a new sheet without reloading the page, append the new name to sheets state, switch activeSheet to it, and write initial data to localStorage.

const newSheetName = `Sheet${sheets.length + 1}`;
setSheets((prev) => [...prev, newSheetName]);
setActiveSheet(newSheetName);
localStorage.setItem(`gridsheet_demo5.${newSheetName}`, JSON.stringify(newSheetData));

Because initialCellsForSheets is memoized on sheets, the new entry is computed automatically on the next render.

Keeping Summary Rows Fixed with sortFixed and filterFixed

Row-level options sortFixed and filterFixed prevent a specific row from being reordered during sort or hidden during filter operations. Set them on the row's header cell using the '0<row>' address format.

buildInitialCells({
  cells: {
    B6: { value: 'Total' },
    C6: { value: '=SUM(C1:C5)' },
    '6': { style: { borderTop: '3px double #000' } },
    '06': { sortFixed: true, filterFixed: true },
  },
})
  • sortFixed: The row stays in place when any column is sorted.
  • filterFixed: The row is never hidden by filter operations.

Rows with these flags show ‹ › markers in their row header automatically via CSS.