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:
- Open DevTools → Performance tab
- Start recording, scroll, stop
- Look for "Layout Thrashing" or long tasks > 50ms
- 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(...)
- Valid functions:
- #DIV/0! → Division by zero
- Fix:
=IF(B1=0, 0, A1/B1)
- Fix:
- #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
| Browser | Support | Issues |
|---|---|---|
| Chrome 90+ | ✅ Full | None known |
| Firefox 88+ | ✅ Full | Clipboard may need permission |
| Safari 14+ | ⚠️ Partial | Some CSS Grid limitations |
| Edge 90+ | ✅ Full | None known |
| IE 11 | ❌ Not supported | No 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
- Check DevTools Console: Look for TypeScript or React errors
- Enable Debug Logging:
localStorage.setItem("debug", "gridheim:*"); - Check Redux State: Install Redux DevTools extension
- Review API Response: Network tab → look for 4xx/5xx errors
- Check Component Props: Verify
workbookId,sheetId, etc. are correct
✅ Most issues resolve by clearing browser cache and restarting.