Headless UI

Use any preferred state management library

At Spreadsheet, we understand the importance of flexibility, which is why our Headless UI approach allows you to use your preferred state management library with all components. Whether you prefer immer, redux, or any other state management library, you can seamlessly integrate it into your workflow.

For those who prefer the ease of use that comes with a pre-built solution, we offer the @rowsncolumns/spreadsheet-state package. This package uses immer to manage spreadsheet state, including sheets, sheetData, and more.

Additionally, it provides functionality for undo/redo, broadcasting history for collaboration, and easy integration with formula parser and evaluation libraries. For a full list of hooks that trigger side effects like formula calculation, history, and broadcasting, check out useSpreadsheetState.

Feel free to take a look at useSpreadsheetState for all hooks that triggers side effects (formula calculation, history and broadcasting)

CellData

To store individual cell information, an interface type CellData is used, a data type that can be stored in an array or record to build sheet data. CellData is a versatile data type that includes all the necessary information to build a robust and functional spreadsheet.

A cellData type looks like the below.

export type CellData = {
  /**
   * The value the user entered in the cell. e.g, 1234 , 'Hello' , or =NOW() Note: Dates, Times and DateTimes are represented as doubles in serial number format.
   * errorValue: Captures data validation errors
   */
  userEnteredValue?: ExtendedValue;
  /**
   * The effective value of the cell. For cells with formulas, this is the calculated value. For cells with literals, this is the same as the userEnteredValue. This field is read-only.
   *
   * errorValue: Capture errors from formula calculation
   */
  effectiveValue?: ExtendedValue;
  /**
   * The formatted value of the cell. This is the value as it's shown to the user. This field is read-only.
   */
  formattedValue?: string;
  /**
   * Text format runs
   */
  textFormatRuns?: TextFormatRun[];
  /**
   * Hyperlink URL
   */
  hyperlink?: string;
  /**
   * Validation applied to a cell
   */
  dataValidation?: DataValidationRule;
  /**
   * Image url
   */
  imageUrl?: string;
  /**
   * Cell meta style
   */
  metaType?: "people";
  /**
   * The format the user entered for the cell. When writing, the new format will be merged with the existing format.
   */
  userEnteredFormat?: CellFormat;
  /**
   * The effective format being used by the cell. This includes the results of applying any conditional formatting and, if the cell contains a formula, the computed number format. If the effective format is the default format, effective format will not be written. This field is read-only.
   */
  effectiveFormat?: CellFormat;
};

Using your own state

Here we are using React useState hook to manage Spreadsheet state.

Do note that you will have to create your own undo/redo data structure for useState. Immer or MobX generates patches for each state update, which can then be undo'ed or redo'ed.

import {
  SpreadsheetProvider,
  CanvasGrid,
  CellData,
  Sheet,
} from "@rowsncolumns/spreadsheet";
import { CellInterface } from "@rowsncolumns/grid";
import { useState } from "react";

export type SheetData<T extends CellData = CellData> = Record<
  string,
  RowData<T>[]
>;
export type RowData<T> = {
  values?: T[];
};

const MySpreadsheet = () => {
  const activeSheet = 1;
  const [sheetData, setSheetData] = useState<SheetData>({
    1: [
      {},
      {
        values: [
          {},
          {
            formattedValue: "Foobar",
          },
        ],
      },
    ],
  });
  const [sheets, setSheets] = useState<Sheet[]>([
    {
      sheetId: 1,
      rowCount: 10,
      columnCount: 10,
      title: "Sheet 1",
    },
  ]);
  return (
    <CanvasGrid
      rowCount={1000}
      columnCount={1000}
      sheetId={activeSheet}
      onChange={(
        sheetId: number,
        cell: CellInterface,
        value: string,
        previousValue: string
      ) => {
        // Save it back to state or database
      }}
      getCellData={(sheetId: number, rowIndex: number, columnIndex: number) => {
        return sheetData[sheetId]?.[rowIndex].values?.[columnIndex];
      }}
    />
  );
};

const App = () => (
  <SpreadsheetProvider>
    <MySpreadsheet />
  </SpreadsheetProvider>
);

Custom CellData

You can inject a custom CellData type to Spreadsheet, and all renderers and editors will infer this type.

type CustomCellData extends CellData {
  myNewProperty?: boolean
}

const MySpreadsheet = () => {
  return (
    <CanvasGrid<CustomCellData>
      rowCount={10}
      columnCount={10}
      getCellData={(sheetId, rowIndex, columnIndex) => {
        return {
          myNewProperty: false
        }
      }}
    />
  )
}

Last updated