Rows n' Columns

3 min read Original article ↗

Spreadsheet 2 - Composable, declarative Spreadsheet component.

Beautifully designed Spreadsheet component for React. Composable. Customizable. Declarative

Current version:

@rowsncolumns/spreadsheet

7.1.267

@rowsncolumns/spreadsheet-state

20.0.122

Built for Developers

Spreadsheet is rendered in HTML Canvas, giving you ~60fps rendering performance and ability to display millions of rows and columns without peformance impact. With escape hatches, you can access the internals and customize it to your liking.

Compose the perfect Spreadsheet

Pick and choose the components you need to build your Spreadsheet.

You can also build own custom functionality on top of Spreadsheet 2, with the many callbacks from CanvasGrid.

import { 
  SpreadsheetProvider, CanvasGrid, Toolbar, ButtonUndo, 
  ButtonRedo, RangeSelector, FormulaBarLabel, FormulaBarInput, 
  BottomBar, SheetSwitcher, SheetTabs, SheetStatus 
} from "@rowsncolumns/spreadsheet"

const App = () => {
  const sheet = { id: 1, rowCount: 1000, columnCount: 1000 }
  return (
    <>
      <Toolbar>
        <ButtonUndo onClick={} />
        <ButtonRedo onClick={} />
        {/*..import all other controls*/}
      </Toolbar>
      <FormulaBar>
        <RangeSelector />
        <FormulaBarInput />
      </FormulaBar>
      <CanvasGrid
        sheetId={sheet.id}
        rowCount={sheet.rowCount}
        columnCount={sheet.columnCount}
        getCellData={(sheetId: number, rowIndex: number, columnIndex: number): CellData => {
          return {
            userEnteredValue: {
              formulaValue: '=SUM(4,4)'
            },
            formattedValue: "8"
          }
        }}
      />
      <BottomBar>
        <SheetSwitcher />
        <SheetTabs />
        <SheetStatus />
      </BottomBar>
    </>
  )
}

{/* Wrap your app in SpreadsheetProvider */}
export const Spreadsheet = () => (
  <SpreadsheetProvider>
    <App />
  </SpreadsheetProvider>
)

Bring your own Data model and State management

Or use `useSpreadsheetState` hook

Spreadsheet components are all uncontrolled and stateless. It renders based on the props that you pass-in and invokes callbacks to user actions.

Spreadsheet 2 is agnostic of your data persistence model. You can choose any database for storage or real-time collaboration.

import type { CellData } from "@rowsncolumns/spreadsheet"
import {
  useSpreadsheetState,
  Sheet,
  SheetData,
  RowData
} from "@rowsncolumns/spreadsheet-state"

type MyCellData = CellData & {
  customProperty: 'hello'
}
type SheetData<T extends CellData> = Record<number, RowData<T>[]>
type RowData<T> = {
  values: T[]
}

const App = () => {
  const [ sheets, onChangeSheets ] = useState<Sheet[]>([])
  const [ sheetData, onChangeSheetData ] = useState<SheetData<MyCellData>>({ })
  return (
    <CanvasGrid<MyCellData>
      getCellData={(sheetId, rowIndex, columnIndex) => {
        return sheetData[sheetId]?.[rowIndex]?.values?.[columnIndex]
      }}
      onChange={(value: string, sheetId: number, rowIndex: number, columnIndex: number) => {
        // Persist and generate undo/redo patches
        // If you are using useSpreadsheetState hook,its all built-in
        onChangeSheetData(prevData => ...)
      }}
    />
  )
}

{/* Wrap your app in SpreadsheetProvider */}
export const Spreadsheet = () => (
  <SpreadsheetProvider>
    <App />
  </SpreadsheetProvider>
)

useSpreadsheetState hook

This is an optional (Production grade) hook if you want to get started with the Spreadsheet with complete state management.

  • Uses immer for state management.
  • Full undo/redo capability.
  • Calculation framework with optional web worker support.
  • Real time collaboration built-in.
import { 
  defaultSpreadsheetTheme,
  Sheet,
  SheetData,
} from "@rowsncolumns/spreadsheet-state"
import { 
  CellData,
  NamedRange,
  EmbeddedObject,
  EmbeddedChart,
  TableView,
  SpreadsheetTheme,
  CanvasGrid,
  SpreadsheetProvider
} from "@rowsncolumns/spreadsheet"

const App = () => {
  const [sheets, onChangeSheets] = useState<Sheet[]>([]);
  const [sheetData, onChangeSheetData] = useState<SheetData<CellData>>({});
  const [scale, onChangeScale] = useState(1);
  const [colorMode, onChangeColorMode] = useState<ColorMode>();
  const [charts, onChangeCharts] = useState<EmbeddedChart[]>([]);
  const [embeds, onChangeEmbeds] = useState<EmbeddedObject[]>([]);
  const [tables, onChangeTables] = useState<TableView[]>([]);
  const [namedRanges, onChangeNamedRanges] = useState<NamedRange[]>();
  const [theme, onChangeTheme] = useState<SpreadsheetTheme>(defaultSpreadsheetTheme);
  const locale = "en-GB";
  const currency = "USD";
  const {
    activeCell,
    activeSheetId,
    selections,
    rowCount,
    columnCount,
    frozenColumnCount,
    frozenRowCount,
    rowMetadata,
    columnMetadata,
    merges,
    protectedRanges,
    bandedRanges,
    conditionalFormats,
    isDarkMode,
    spreadsheetColors,
    canRedo,
    canUndo,
    undo,
    redo,    
    ...// other Spreadsheet methods
  } = useSpreadsheetState({
    sheets,
    sheetData,
    tables,
    functions,
    namedRanges,
    theme,
    colorMode,
    locale,
    onChangeSheets,
    onChangeSheetData,
    onChangeEmbeds,
    onChangeCharts,
    onChangeTables,
    onChangeNamedRanges,
    onChangeTheme,
  })
  return (
    <>
      <Toolbar>
      <ButtonUndo onClick={onUndo} disabled={!canUndo} />
      <ButtonRedo onClick={onRedo} disabled={!canRedo} />
        {/*..import all other controls*/}
      </Toolbar>

      <CanvasGrid
        {...spreadsheetColors}
        enableTextOverflow
        stickyEditor={true}
        scale={scale}
        conditionalFormats={conditionalFormats}
        sheetId={activeSheetId}
        rowCount={rowCount}
        columnCount={columnCount}
        frozenColumnCount={frozenColumnCount}
        frozenRowCount={frozenRowCount}
        rowMetadata={rowMetadata}
        columnMetadata={columnMetadata}
        activeCell={activeCell}
        selections={selections}
        theme={theme}
        merges={merges}
        charts={charts}
        embeds={embeds}
        tables={tables}
        {/* Other methods from the hook */}
      />
    </>
  )
}

{/* Wrap your app in SpreadsheetProvider */}
export const Spreadsheet = () => (
  <SpreadsheetProvider>
    <App />
  </SpreadsheetProvider>
)