Case 5: 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
Section titled “Implementation Guide”One useSheetRef / useStoreRef Per Sheet
Section titled “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
Section titled “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 / RedostoreHandle.dispatch(userActions.undo(null));storeHandle.dispatch(userActions.redo(null));
// Cut requires the current selection areaconst area = clip(store);storeHandle.dispatch(userActions.cut(areaToZone(area)));
// CopystoreHandle.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
Section titled “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
Section titled “Per-sheet localStorage Keys”Each sheet is stored independently under a namespaced key so sheets don’t overwrite each other.
// SavelocalStorage.setItem(`gridsheet_demo5.${activeSheet}`, JSON.stringify(saveData));
// Load recentconst 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
Section titled “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
Section titled “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
Section titled “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.