Most spreadsheet libraries are really two things glued together: a grid that draws cells on screen, and an engine that figures out what those cells should contain. HyperFormula rips those apart and keeps only the second half. It is a headless calculation engine — no UI, no DOM, no rendering. You feed it cells and formulas, it parses them, builds a dependency graph, evaluates everything in the right order, and hands you back values. The pixels are entirely your problem.
That separation is the whole point. If you are building a custom data grid and want Excel-grade math underneath it, HyperFormula slots in cleanly. If you need to recompute an uploaded spreadsheet on a Node.js server, run a financial model, or validate user-submitted formulas, the engine runs there too with the exact same API. Built in TypeScript by the team behind Handsontable, it ships 400+ functions compatible with Microsoft Excel and Google Sheets, named expressions, undo/redo, cross-sheet references, and i18n — all while staying completely decoupled from how you choose to present the results.
Why a Headless Engine Earns Its Keep
The engine works in three phases, and understanding them explains most of its behavior. First, every formula like 7*3-SIN(A5) is tokenized and parsed into an Abstract Syntax Tree using Chevrotain. Second, the engine maps cell-to-cell relationships into a directed graph and computes a topological order, so A1 and B1 are always evaluated before C1 = A1+B1. Circular references are caught right here and surfaced as #CYCLE! errors. Third, cells are evaluated in dependency order — and crucially, when one cell changes, only the affected subgraph recomputes rather than the entire sheet.
A few design choices keep memory and CPU in check at scale:
- Relative addressing and shared ASTs. References are stored as offsets, not absolute coordinates. A formula filled down a column is structurally identical in every row, so those cells share a single AST instead of duplicating it.
- Lazy CRUD transformations. Adding, removing, or moving rows and columns does not immediately rewrite every affected formula. The engine logs the operations and applies them only when a formula is read or recalculated.
- Incremental recalculation. The headline feature — changes ripple only through the dependency subgraph they touch, which is the core win over naive "recompute everything" parsers.
It is also remarkably lean. The runtime has just two dependencies: chevrotain for parsing and tiny-emitter for events. It runs in modern browsers and Node.js, and ships its own TypeScript definitions.
Getting It Into Your Project
Installation is a single package.
npm install hyperformula
yarn add hyperformula
One thing to know before your first line of code: a license key is mandatory, even for open-source use. Pass licenseKey: 'gpl-v3' for GPL usage, or your commercial key otherwise. Forget it and the console fills with warnings. We will cover the licensing nuance in detail near the end, because it genuinely affects whether this library is right for your product.
Computing Your First Cells
The fastest way to get a working engine is to build it from a 2D array. Rows go in, computed values come out.
import { HyperFormula } from 'hyperformula';
const hf = HyperFormula.buildFromArray(
[[1, 2, '=A1+B1']],
{ licenseKey: 'gpl-v3' }
);
hf.getCellValue({ sheet: 0, row: 0, col: 2 }); // 3
You address cells with a { sheet, row, col } object rather than A1-style strings, which is friendlier for programmatic access. The formula in the third cell references A1 and B1, the engine wires up the dependency, and getCellValue returns the evaluated result.
Mutations recalculate automatically, and you can always inspect the raw formula separately from its computed value.
hf.setCellContents({ sheet: 0, row: 0, col: 0 }, [['10']]);
hf.getCellValue({ sheet: 0, row: 0, col: 2 }); // 12 — recalculated for you
hf.getCellFormula({ sheet: 0, row: 0, col: 2 }); // '=A1+B1'
hf.getSheetValues(0); // the whole sheet of computed values
Notice that changing A1 to 10 automatically pushed the dependent cell to 12. You never told it to recalculate — the dependency graph did that work, and only for the cells that actually needed it.
Modelling Real Numbers, Not Just Toy Sums
Beyond buildFromArray, there are two other entry points: buildEmpty for starting blank, and buildFromSheets for spinning up multiple named sheets at once. Combine an empty engine with named expressions and you get something that reads like an actual financial model rather than a wall of cell coordinates.
const hf = HyperFormula.buildEmpty({ licenseKey: 'gpl-v3' });
const sheetName = hf.addSheet('Mortgage Calculator');
const sheetId = hf.getSheetId(sheetName);
hf.addNamedExpression('AnnualInterestRate', '8%');
hf.addNamedExpression('NumberOfMonths', 360);
hf.addNamedExpression('LoanAmount', 800000);
hf.setCellContents({ sheet: sheetId, row: 0, col: 0 }, [[
'Monthly Payment',
'=PMT(AnnualInterestRate/12, NumberOfMonths, -LoanAmount)'
]]);
Named expressions are HyperFormula's take on Excel's named ranges: named constants or formulas you can reference from any cell, with either global or per-sheet scope. The payoff is readability. A formula that says =PMT(AnnualInterestRate/12, NumberOfMonths, -LoanAmount) documents itself, while =PMT(B2/12, B3, -B4) forces every reader to go hunting. For budgeting, pricing, and what-if calculators, this turns brittle coordinate soup into something maintainable.
And PMT is just one of roughly 400 functions on offer — math and trig, statistical, financial (IRR, NPV, FV), logical, text, date/time, lookups (VLOOKUP, INDEX, MATCH, XLOOKUP), and array functions. Because the syntax matches Excel and Google Sheets, formula strings copied out of a real spreadsheet generally just work. There are even 17 built-in languages for function names, so German users can write SUMME instead of SUM.
Teaching It New Tricks With Custom Functions
When the 400 built-ins do not cover your domain, you can register your own through the plugin API. You extend FunctionPlugin, declare the function's metadata, supply translations, and register it.
import { FunctionPlugin, FunctionArgumentType } from 'hyperformula';
export class MyCustomPlugin extends FunctionPlugin {
greet(ast, state) {
return this.runFunction(ast.args, state, this.metadata('GREET'),
(firstName) => `Hello, ${firstName}!`);
}
}
MyCustomPlugin.implementedFunctions = {
GREET: {
method: 'greet',
parameters: [{ argumentType: FunctionArgumentType.STRING }],
},
};
export const MyCustomPluginTranslations = {
enGB: { GREET: 'GREET' },
enUS: { GREET: 'GREET' },
};
HyperFormula.registerFunctionPlugin(MyCustomPlugin, MyCustomPluginTranslations);
Once registered, =GREET("Mittens") becomes a first-class formula that participates in the dependency graph and recalculation exactly like a built-in. This is how teams bake business-specific logic — proprietary pricing rules, internal scoring formulas — directly into the engine instead of post-processing values after the fact.
Going Fast With Batched Mutations
Incremental recalculation is already smart, but when you are loading a thousand cells you do not want a recalc after every single write. Wrap the writes in batch and the engine recalculates once at the end, returning a single combined changeset.
const changes = hf.batch(() => {
hf.setCellContents({ sheet: 0, row: 0, col: 3 }, [['=B1']]);
hf.setCellContents({ sheet: 0, row: 0, col: 4 }, [['=A1']]);
}); // one recalculation, one changeset
For long-running bulk work — think server-side imports — you can take manual control instead with suspendEvaluation() and resumeEvaluation(), checking status via isEvaluationSuspended(). There is one sharp edge to respect: read operations like getCellValue, getSheetSerialized, and paste will throw if you call them inside a batch() callback or while evaluation is suspended. Write first, resume, then read. Beyond batching, advanced tuning knobs include useColumnIndex to speed up lookups on large unsorted data, configurable address-mapping policies (AlwaysDense, AlwaysSparse, or auto), and maxPendingLazyTransformations to balance the memory-versus-CPU tradeoff of those lazy transformations. Marketed as comfortable with millions of cells, the relative-addressing and shared-AST design is what keeps that claim honest.
Living Inside React (and Friends)
There is no official React wrapper, and that is by design — HyperFormula is framework-agnostic, so the integration story is the same everywhere: import it and instantiate it. In React, the engine is a long-lived imperative object, so the clean pattern is to hold it in a useRef, initialize it in useEffect, and call hf.destroy() in the cleanup function.
import { useEffect, useRef, useState } from 'react';
import { HyperFormula } from 'hyperformula';
function useSpreadsheet(initialData: (string | number)[][]) {
const hfRef = useRef<HyperFormula | null>(null);
const [values, setValues] = useState<unknown[][]>([]);
useEffect(() => {
const hf = HyperFormula.buildFromArray(initialData, {
licenseKey: 'gpl-v3',
});
hfRef.current = hf;
setValues(hf.getSheetValues(0));
return () => {
hf.destroy();
hfRef.current = null;
};
}, []);
return { hf: hfRef.current, values, setValues };
}
Keeping displayed values in useState and refreshing them via getSheetValues() after each mutation gives React the reactive surface it expects, while the engine stays imperative underneath. The destroy() call in cleanup also handles React 18 StrictMode's double-mount correctly. If you are on the Next.js App Router, load any component that touches the engine through a client-only dynamic import so the engine never sneaks into the server bundle. The same import-and-instantiate approach is documented for Angular, Vue, and Svelte.
The License Conversation You Should Have First
Here is the detail that deserves a clear-eyed read before you commit: HyperFormula is dual-licensed under GPLv3 and a separate commercial license, and "open source" here means GPLv3, not MIT. That distinction matters.
The free licenseKey: 'gpl-v3' option is genuinely open source, but GPLv3 is copyleft. If you distribute software that links HyperFormula, those obligations can require you to release your own source under a compatible license. For a closed-source product or a commercial SaaS you ship to customers, that is a real constraint, not a footnote. The alternative is a commercial license purchased from Handsontable's sales team, which uses a key in the 'xxxx-xxxx-xxxx-xxxx-xxxx' format and frees you from the copyleft terms. Pricing is not published; you contact sales.
License validation is fully offline — there is no phone-home. The library simply compares its own build date against the date embedded in your key, and a missing, invalid, or expired key produces console notifications. The practical takeaway: if you are shipping a proprietary app, budget for the commercial license up front, and weigh that against MIT-licensed alternatives like @formulajs/formulajs (a function library, not a full engine) or fast-formula-parser (a fast parser without the dependency graph, CRUD, or undo/redo).
Should You Reach For It?
HyperFormula is the most complete spreadsheet engine in the JavaScript ecosystem — dependency graph, incremental recalculation, CRUD, ~400 Excel-compatible functions, named expressions, i18n, and undo/redo, all without a single line of rendering code. That headless design is liberating when you own the UI or run calculations on the server, and the lean two-dependency footprint keeps it honest. The 400-function library and Excel/Google-Sheets compatibility mean you are not reinventing financial math, and the plugin API lets you extend it where your domain needs more.
The one thing to decide deliberately is licensing. For internal tools, open-source projects, or anything you are comfortable shipping under GPLv3, the free key is all you need. For proprietary products, factor in the commercial license before you build a dependency on it. Get that decision right and HyperFormula gives you a spreadsheet's brain without ever making you draw its body.