Skip to content

Plugin Architecture

@witqq/spreadsheet uses a lightweight plugin system that lets you extend the engine without modifying core code. Plugins can add render layers, listen to events, store isolated state, and integrate with undo/redo.

Live Demo
Toggle plugins on and off to see their effects. Formula calculates Sum column. Conditional formatting applies gradient to Score.
View source code
PluginShowcaseDemo.tsx
import { useRef, useState, useCallback } from 'react';
import { Spreadsheet } from '@witqq/spreadsheet-react';
import type { SpreadsheetRef } from '@witqq/spreadsheet-react';
import type { ColumnDef } from '@witqq/spreadsheet';
import { FormulaPlugin, ConditionalFormattingPlugin } from '@witqq/spreadsheet-plugins';
import { DemoWrapper } from './DemoWrapper';
import { DemoButton } from './DemoButton';
import { DemoToolbar } from './DemoToolbar';
import { useSiteTheme } from './useSiteTheme';
const columns: ColumnDef[] = [
{ key: 'a', title: 'Value A', width: 100, type: 'number' },
{ key: 'b', title: 'Value B', width: 100, type: 'number' },
{ key: 'c', title: 'Sum (A+B)', width: 120 },
{ key: 'score', title: 'Score', width: 100, type: 'number' },
];
const initialData = [
{ a: 10, b: 20, c: '', score: 85 },
{ a: 25, b: 15, c: '', score: 42 },
{ a: 30, b: 10, c: '', score: 95 },
{ a: 5, b: 45, c: '', score: 28 },
{ a: 15, b: 35, c: '', score: 73 },
{ a: 40, b: 20, c: '', score: 58 },
{ a: 20, b: 30, c: '', score: 91 },
{ a: 35, b: 5, c: '', score: 15 },
{ a: 8, b: 42, c: '', score: 67 },
{ a: 50, b: 10, c: '', score: 100 },
];
export function PluginShowcaseDemo() {
const { witTheme } = useSiteTheme();
const tableRef = useRef<SpreadsheetRef>(null);
const [formulaEnabled, setFormulaEnabled] = useState(false);
const [condFormatEnabled, setCondFormatEnabled] = useState(false);
const formulaPluginRef = useRef<FormulaPlugin | null>(null);
const condFormatPluginRef = useRef<ConditionalFormattingPlugin | null>(null);
const toggleFormula = useCallback(() => {
const engine = tableRef.current?.getInstance();
if (!engine) return;
if (formulaEnabled) {
engine.removePlugin('formula');
formulaPluginRef.current = null;
for (let row = 0; row < 10; row++) {
engine.setCell(row, 2, '');
}
engine.requestRender();
} else {
const plugin = new FormulaPlugin({ syncOnly: true });
engine.installPlugin(plugin);
formulaPluginRef.current = plugin;
for (let row = 0; row < 10; row++) {
const formula = `=A${row + 1}+B${row + 1}`;
engine.setCell(row, 2, formula);
engine.getEventBus().emit('cellChange', {
row,
col: 2,
value: formula,
column: columns[2],
oldValue: '',
newValue: formula,
source: 'edit' as const,
});
}
engine.requestRender();
}
setFormulaEnabled(!formulaEnabled);
}, [formulaEnabled]);
const toggleCondFormat = useCallback(() => {
const engine = tableRef.current?.getInstance();
if (!engine) return;
if (condFormatEnabled) {
engine.removePlugin('conditional-format');
condFormatPluginRef.current = null;
engine.requestRender();
} else {
const plugin = new ConditionalFormattingPlugin();
engine.installPlugin(plugin);
plugin.addRule(
ConditionalFormattingPlugin.createGradientScale(
{ startRow: 0, startCol: 3, endRow: 9, endCol: 3 },
[
{ value: 0, color: '#ef4444' },
{ value: 50, color: '#eab308' },
{ value: 100, color: '#22c55e' },
],
),
);
condFormatPluginRef.current = plugin;
engine.requestRender();
}
setCondFormatEnabled(!condFormatEnabled);
}, [condFormatEnabled]);
return (
<DemoWrapper
height={440}
title="Live Demo"
description="Toggle plugins on and off to see their effects. Formula calculates Sum column. Conditional formatting applies gradient to Score."
>
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<DemoToolbar>
<DemoButton variant="toggle" active={formulaEnabled} onClick={toggleFormula}>
Formula Plugin: {formulaEnabled ? 'ON' : 'OFF'}
</DemoButton>
<DemoButton variant="toggle" active={condFormatEnabled} onClick={toggleCondFormat}>
Conditional Formatting: {condFormatEnabled ? 'ON' : 'OFF'}
</DemoButton>
</DemoToolbar>
<div style={{ flex: 1 }}>
<Spreadsheet
theme={witTheme}
ref={tableRef}
columns={columns}
data={initialData}
editable={true}
showRowNumbers={true}
style={{ width: '100%', height: '100%' }}
/>
</div>
</div>
</DemoWrapper>
);
}

Every plugin implements SpreadsheetPlugin:

interface SpreadsheetPlugin {
readonly name: string;
readonly version: string;
readonly dependencies?: string[];
install(api: PluginAPI): void;
destroy?(): void;
}
FieldDescription
nameUnique identifier (e.g. 'formula', 'conditional-format')
versionSemver string
dependenciesOptional array of plugin names that must be installed first
install(api)Called when the plugin is added to the engine. If the engine is not yet mounted, install() is deferred until engine.mount() is called
destroy()Optional cleanup — plugins must manually unbind event handlers and remove layers

The install method receives a PluginAPI object:

interface PluginAPI {
readonly engine: SpreadsheetEngine;
getPluginState<T>(key: string): T | undefined;
setPluginState<T>(key: string, value: T): void;
}
  • engine — full access to SpreadsheetEngine (event bus, cell store, render pipeline, etc.)
  • getPluginState / setPluginState — isolated key-value storage per plugin, managed by the engine
import { SpreadsheetEngine } from '@witqq/spreadsheet';
const engine = new SpreadsheetEngine(config);
engine.installPlugin(myPlugin);
engine.removePlugin('my-plugin');

When engine.destroy() is called, plugins are destroyed in reverse installation order.

const tableRef = useRef<SpreadsheetRef>(null);
// After mount
tableRef.current?.installPlugin(myPlugin);
tableRef.current?.removePlugin('my-plugin');
PluginPackageDescription
FormulaPlugin@witqq/spreadsheet-pluginsSpreadsheet formulas with dependency graph. Supports worker option for off-thread calculation
ConditionalFormattingPlugin@witqq/spreadsheet-pluginsColor scales, data bars, icon sets
ExcelPlugin@witqq/spreadsheet-pluginsImport/export .xlsx via lazy-loaded SheetJS
createContextMenuPlugin@witqq/spreadsheet-pluginsRight-click context menu with custom items
CollaborationPlugin@witqq/spreadsheet-pluginsReal-time OT collaboration with remote cursors
ProgressiveLoaderPlugin@witqq/spreadsheet-pluginsNon-blocking large dataset loading with progress overlay

A plugin that highlights the active row:

import type { SpreadsheetPlugin, PluginAPI, SelectionChangeEvent } from '@witqq/spreadsheet';
const activeRowPlugin: SpreadsheetPlugin = {
name: 'active-row-highlight',
version: '1.0.0',
install(api: PluginAPI) {
const { engine } = api;
const handler = (event: SelectionChangeEvent) => {
const row = event.selection.activeCell.row;
api.setPluginState('activeRow', row);
engine.requestRender();
};
engine.on('selectionChange', handler);
api.setPluginState('handler', handler);
},
destroy() {
// Plugins must manually unbind event handlers here.
// The engine does NOT auto-unbind handlers on plugin removal.
},
};
import { Spreadsheet, SpreadsheetRef } from '@witqq/spreadsheet-react';
import { FormulaPlugin, ConditionalFormattingPlugin } from '@witqq/spreadsheet-plugins';
function App() {
const ref = useRef<SpreadsheetRef>(null);
useEffect(() => {
ref.current?.installPlugin(new FormulaPlugin());
ref.current?.installPlugin(new ConditionalFormattingPlugin());
return () => {
ref.current?.removePlugin('formula');
ref.current?.removePlugin('conditional-format');
};
}, []);
return <Spreadsheet ref={ref} columns={columns} data={data} />;
}