The HyperFormula API is identical in a React app and in plain JavaScript. What changes is where the engine lives in a component tree and how its lifecycle maps to React hooks.
Install with npm install hyperformula. For other options, see the client-side installation section.
Hold the HyperFormula instance in a useRef so it survives re-renders. Initialize it inside useEffect and release it in the cleanup function. Use useState to toggle between raw formulas and computed values.
import { useEffect, useRef, useState } from 'react';
import { HyperFormula, CellValue } from 'hyperformula';
export function SpreadsheetComponent() {
const hfRef = useRef<HyperFormula | null>(null);
const [values, setValues] = useState<CellValue[][]>([]);
useEffect(() => {
const hf = HyperFormula.buildFromArray(
[
[1, 2, '=A1+B1'],
// your data rows go here
],
{
licenseKey: 'gpl-v3',
// more configuration options go here
}
);
hfRef.current = hf;
return () => {
hf.destroy();
hfRef.current = null;
};
}, []);
function runCalculations() {
if (!hfRef.current) return;
setValues(hfRef.current.getSheetValues(0));
}
function reset() {
setValues([]);
}
return (
<>
<button onClick={runCalculations}>Run calculations</button>
<button onClick={reset}>Reset</button>
{values.length > 0 && (
<table>
<tbody>
{values.map((row, r) => (
<tr key={r}>
{row.map((cell, c) => (
<td key={c}>{String(cell ?? '')}</td>
))}
</tr>
))}
</tbody>
</table>
)}
</>
);
}If you use JavaScript instead of TypeScript, drop the type annotations — the rest of the pattern is unchanged.
In development, React 18 runs effects twice (mount → unmount → mount) to surface cleanup bugs. The pattern above is correct for StrictMode because destroy() runs before the re-mount creates a new instance, so no work leaks between the two lifecycles. Do not switch to a module-scoped singleton as a workaround — it will break StrictMode semantics.
HyperFormula depends on browser-only APIs. Mark SpreadsheetComponent.tsx with 'use client', then load it lazily from a server page component using dynamic(..., { ssr: false }):
// app/spreadsheet/SpreadsheetComponent.tsx
'use client';
// ... component definition as above// app/spreadsheet/page.tsx ← server component, no 'use client'
import dynamic from 'next/dynamic';
const SpreadsheetComponent = dynamic(
() => import('./SpreadsheetComponent').then((m) => m.SpreadsheetComponent),
{ ssr: false }
);
export default function Page() {
return <SpreadsheetComponent />;
}Putting 'use client' on page.tsx itself would make the entire page a client component — the point is to keep the page server-rendered and only hydrate the spreadsheet widget on the client. In the Pages Router, the same dynamic(..., { ssr: false }) pattern works without 'use client'.
- Configuration options — full list of
buildFromArray/buildEmptyoptions - Basic operations — CRUD on cells, rows, columns, sheets
- Advanced usage — multi-sheet workbooks, named expressions
- Custom functions — register your own formulas
For a more advanced example, check out the React demo on Stackblitz.