Skip to main content

Spreadsheet Engine (Gridheim)

Debug common issues with the Gridheim spreadsheet component.

Performance Issues

Symptom: Grid freezes with 1000+ cells

Cause: Virtual scrolling not properly configured or disabled

Solution:

// ❌ WRONG: renders all cells
<Gridheim workbookId="wb-123" disableVirtualization={true} />

// ✅ CORRECT: uses virtual scrolling
<Gridheim workbookId="wb-123" rows={10000} cols={26} />

Verify:

  • Check browser DevTools: Performance → Record → Scroll
  • Should see <16ms frames (60 FPS)
  • Memory usage should plateau around 50MB for 10K cells

Symptom: Scroll is laggy or stuttering

Cause: Expensive computations during render

Solution:

// Memoize cell content component
const MemoCell = React.memo(({ cell, isSelected }) => (
<div style={{ background: isSelected ? '#e3f2fd' : 'white' }}>
{cell.value}
</div>
));

// Use in grid
<Gridheim renderCell={() => <MemoCell />} />

Debug with Chrome DevTools:

  1. Open DevTools → Performance tab
  2. Start recording, scroll, stop
  3. Look for "Layout Thrashing" or long tasks > 50ms
  4. Check "Rendering" stats at bottom

Symptom: Memory leaks after editing many cells

Cause: Event listeners not cleaning up or Redux state not pruning

Solution:

// Clear old cells from cache periodically
const MAX_CACHE_SIZE = 5000;

dispatch(
gridSlice.actions.pruneCache({
maxSize: MAX_CACHE_SIZE,
keepRecent: true,
})
);

Editing & Formula Issues

Symptom: Formula doesn't recalculate when dependent cell changes

Cause: Formula dependency tracking not updated

Solution:

// ✅ Correct way to update cell
const handleCellChange = async (address: string, value: string) => {
// Update cell
await updateCell({ address, value }).unwrap();

// Trigger dependent formula recalculation
dispatch(gridSlice.actions.invalidateDependents(address));
};

Symptom: Error: "Circular reference detected"

Cause: Formula contains cyclic dependency (A1 = B1, B1 = A1)

Solution:

// Gridheim automatically detects circular refs
// Shows error: "Circular reference in cell A1"

// Fix: Edit formula to remove cycle
// A1: =A1 + 1 → Change to =B1 + 1

Validation Code:

import { detectCircularReferences } from "@/components/Gridheim/utils/formulaUtils";

const hasCircle = detectCircularReferences(formulas);
if (hasCircle) {
console.error("Circular reference detected:", hasCircle);
}

Symptom: Formula returns #NAME? error

Cause: Unknown function name or syntax error

Solution:

  • #NAME? → Function doesn't exist
    • Valid functions: SUM, AVERAGE, COUNT, IF, VLOOKUP, etc.
    • Check spelling: =SUMM(...)=SUM(...)
  • #DIV/0! → Division by zero
    • Fix: =IF(B1=0, 0, A1/B1)
  • #REF! → Invalid cell reference
    • Fix: Check cell addresses are valid (A1, B5, etc.)
// Use function list for autocomplete
const VALID_FUNCTIONS = [
"SUM",
"AVERAGE",
"COUNT",
"COUNTA",
"MIN",
"MAX",
"IF",
"AND",
"OR",
"NOT",
"VLOOKUP",
"INDEX",
"MATCH",
"DATE",
"TODAY",
"NOW",
"TEXT",
"CONCATENATE",
"UPPER",
"LOWER",
"TRIM",
];

Symptom: Copy/Paste not working

Cause: Browser clipboard permissions or disabled clipboard access

Solution:

// Check clipboard API support
if (!navigator.clipboard) {
console.error("Clipboard API not supported");
// Fall back to manual selection
}

// Grant clipboard permission
// Chrome: Settings → Privacy → Clipboard
// Firefox: about:config → dom.events.asyncClipboard.clipboardevents

// Use fallback for cross-origin
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(data);
} catch (err) {
// Fallback: select text manually
document.execCommand("copy");
}
};

Data & State Issues

Symptom: Cells show old data after refresh

Cause: RTK Query cache not invalidated

Solution:

// Force refetch from server
const { refetch } = useGetCellsQuery({ workbookId, sheetId });

const handleManualRefresh = () => {
refetch();
};

// Or: Manually invalidate cache
import { gridApi } from "@/redux/services";

dispatch(gridApi.util.invalidateTags(["Cells"]));

Symptom: Cells are lost after navigation away and back

Cause: Redux state cleared by route reset

Solution:

// Persist cells to localStorage
import { persistStore } from "redux-persist";

const persistConfig = {
key: "grid",
storage: localStorage,
};

// Or: Keep cells in Redux
// Ensure slices persist with redux-persist middleware

Symptom: Multiple users editing same sheet cause conflicts

Cause: No conflict resolution or locking mechanism

Solution:

// Implement optimistic locking
const handleCellUpdate = async (address: string, value: string) => {
const cell = getCellFromStore(address);
const currentVersion = cell.version;

try {
await updateCell({
address,
value,
expectedVersion: currentVersion,
}).unwrap();
} catch (err) {
if (err.code === "CONFLICT") {
// Version mismatch: refresh and retry
await refetch();
alert("Cell was updated by another user. Please try again.");
}
}
};

Selection & Navigation Issues

Symptom: Selection not highlighting correctly

Cause: CSS specificity issue or wrong selector state

Solution:

// Ensure selection CSS has high specificity
.cell.selected {
background-color: #e3f2fd !important;
border: 2px solid #2196f3 !important;
z-index: 10;
}

// Verify state is updating
const Cell = ({ address, isSelected }) => {
console.log(`Cell ${address} selected:`, isSelected); // Debug
return (
<div className={isSelected ? 'cell selected' : 'cell'}>
{/* content */}
</div>
);
};

Symptom: Keyboard arrows don't navigate between cells

Cause: Input focus intercepting keyboard events

Solution:

// Ensure proper event delegation
const handleKeyDown = (e: KeyboardEvent) => {
// Only handle if NOT in edit mode
if (isEditing) return;

switch (e.key) {
case 'ArrowUp':
e.preventDefault();
moveSelection('up');
break;
case 'ArrowDown':
e.preventDefault();
moveSelection('down');
break;
// ... etc
}
};

// Attach to grid container, not individual cells
<div ref={gridRef} onKeyDown={handleKeyDown} tabIndex={0}>
{/* cells */}
</div>

Symptom: Can't select range by dragging

Cause: Drag event not properly captured or throttled

Solution:

const handleMouseDown = (address: string) => {
setRangeStart(address);
setIsSelecting(true);
};

const handleMouseOver = (address: string) => {
if (isSelecting && rangeStart) {
// Update selection range
setSelectedRange(rangeStart, address);
}
};

const handleMouseUp = () => {
setIsSelecting(false);
};

// Use event delegation to avoid attaching listeners to each cell
<div onMouseDown={handleMouseDown} onMouseMove={handleMouseOver} onMouseUp={handleMouseUp}>
{/* Grid cells */}
</div>

Styling & Display Issues

Symptom: Column widths don't persist

Cause: Width state not saved to database

Solution:

// Save column widths to Sheet model
const handleColumnResize = async (colIndex: number, newWidth: number) => {
const columnConfig = { ...sheet.columnWidths };
columnConfig[colIndex] = newWidth;

await updateSheet({
id: sheetId,
columnWidths: columnConfig,
}).unwrap();

dispatch(gridSlice.actions.setColumnWidth({ colIndex, width: newWidth }));
};

Symptom: Text overflows cell boundaries

Cause: Cell style missing overflow: hidden or white-space: nowrap

Solution:

// CSS fix
.cell {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}

// Or allow wrapping
.cell.wrapped {
white-space: normal;
word-wrap: break-word;
overflow: visible;
height: auto; /* May need dynamic height */
}

Symptom: Merge cells doesn't display correctly

Cause: Merged cell boundaries not rendered

Solution:

const isMergedCell = (address: string) => {
return sheet.mergedRanges?.some(range =>
isAddressInRange(address, range)
);
};

const getMergeSpan = (address: string) => {
const merge = sheet.mergedRanges?.find(range =>
isAddressInRange(address, range)
);
return merge ? {
colSpan: merge.endCol - merge.startCol + 1,
rowSpan: merge.endRow - merge.startRow + 1
} : { colSpan: 1, rowSpan: 1 };
};

// Render with colSpan/rowSpan
<td colSpan={span.colSpan} rowSpan={span.rowSpan}>
{/* Cell content */}
</td>

Integration Issues

Symptom: ThorAPI mutation not updating cell

Cause: Mutation payload incorrect or response mapping missing

Solution:

// Verify mutation signature
const [updateCell, { isLoading }] = useUpdateCellMutation();

// Must match OpenAPI spec
await updateCell({
workbookId,
sheetId,
address: 'A1',
value: 'new value',
// ... other required fields
}).unwrap();

// Add debug logging
.then(result => console.log('Update successful:', result))
.catch(err => console.error('Update failed:', err));

Symptom: Cells not syncing between tabs

Cause: Redux state isolated per component or tabs have separate stores

Solution:

// Ensure single Redux store shared across tabs
// In app root:
import { Provider } from 'react-redux';
import { store } from '@/redux/store';

<Provider store={store}>
<App />
</Provider>

// Or: Use Web Storage to sync across tabs
const unsubscribe = store.subscribe(() => {
const state = store.getState();
localStorage.setItem('gridState', JSON.stringify(state.grid));
});

// On other tab:
const savedState = JSON.parse(localStorage.getItem('gridState') || '{}');
dispatch(gridSlice.actions.restore(savedState));

Browser Compatibility

BrowserSupportIssues
Chrome 90+✅ FullNone known
Firefox 88+✅ FullClipboard may need permission
Safari 14+⚠️ PartialSome CSS Grid limitations
Edge 90+✅ FullNone known
IE 11❌ Not supportedNo ES6, no CSS Grid

Workaround for older browsers:

// Polyfill for Promise.allSettled (IE11)
if (!Promise.allSettled) {
Promise.allSettled = function (promises) {
return Promise.all(
promises.map((p) =>
Promise.resolve(p)
.then((v) => ({ status: "fulfilled", value: v }))
.catch((e) => ({ status: "rejected", reason: e }))
)
);
};
}

Getting Help

  1. Check DevTools Console: Look for TypeScript or React errors
  2. Enable Debug Logging:
    localStorage.setItem("debug", "gridheim:*");
  3. Check Redux State: Install Redux DevTools extension
  4. Review API Response: Network tab → look for 4xx/5xx errors
  5. Check Component Props: Verify workbookId, sheetId, etc. are correct

Most issues resolve by clearing browser cache and restarting.