diff --git a/eslint.config.js b/eslint.config.js
index a14c2bd..6be1776 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -17,6 +17,9 @@ export default defineConfig(
},
rules: {
'curly': ['error', 'all'],
+ '@typescript-eslint/no-unused-vars': ['error', {
+ argsIgnorePattern: '^_'
+ }],
'@stylistic/semi': ['error', 'always'],
'@stylistic/quotes': ['error', 'single', { avoidEscape: true }],
'@stylistic/brace-style': ['error', '1tbs', { allowSingleLine: true }],
diff --git a/examples/grid-lite/components-react/index.html b/examples/grid-lite/components-react/index.html
new file mode 100644
index 0000000..7f1e258
--- /dev/null
+++ b/examples/grid-lite/components-react/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+ Highcharts Grid Lite - React Example
+
+
+
+
+
+
+
diff --git a/examples/grid-lite/components-react/package.json b/examples/grid-lite/components-react/package.json
new file mode 100644
index 0000000..cd5b45a
--- /dev/null
+++ b/examples/grid-lite/components-react/package.json
@@ -0,0 +1,26 @@
+{
+ "name": "grid-lite-minimal-react",
+ "version": "0.0.0",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc && vite build",
+ "preview": "vite preview",
+ "clean": "rimraf dist node_modules"
+ },
+ "dependencies": {
+ "@highcharts/grid-lite": ">=3.0.0",
+ "@highcharts/grid-lite-react": "workspace:*",
+ "react": ">=18",
+ "react-dom": ">=18"
+ },
+ "devDependencies": {
+ "@types/react": ">=18",
+ "@types/react-dom": ">=18",
+ "@vitejs/plugin-react": "^4.2.0",
+ "typescript": "^5.0.0",
+ "vite": "^5.0.0"
+ }
+}
+
diff --git a/examples/grid-lite/components-react/src/App.tsx b/examples/grid-lite/components-react/src/App.tsx
new file mode 100644
index 0000000..ac64cd3
--- /dev/null
+++ b/examples/grid-lite/components-react/src/App.tsx
@@ -0,0 +1,142 @@
+import { useState, useRef } from 'react';
+import {
+ type GridInstance,
+ type GridRefHandle,
+ type GridOptions,
+ Grid,
+ Caption,
+ Data,
+ DataTable,
+ ColumnDefaults,
+ Column,
+ Description
+} from '@highcharts/grid-lite-react';
+
+function App() {
+ const grid = useRef | null>(null);
+
+ // ==== OPTIONS ====
+ // const [options] = useState({
+ // dataTable: {
+ // columns: {
+ // name: ['1111Alice', 'Bob', 'Charlie', 'David', 'Eve'],
+ // age: [23, 34, 45, 56, 67],
+ // city: ['New York', 'Oslo', 'Paris', 'Tokyo', 'London'],
+ // salary: [50000, 60000, 70000, 80000, 90000]
+ // }
+ // }
+ // });
+
+ // ==== DATA ====
+ // Data Columns
+ const [dataSource, setDataSource] = useState({
+ name: ['COLUMNS', 'Bob', 'Charlie', 'David', 'Eve'],
+ age: [23, 34, 45, 56, 67],
+ city: ['New York', 'Oslo', 'Paris', 'Tokyo', 'London'],
+ salary: [50000, 60000, 70000, 80000, 90000]
+ });
+
+ // Data Table
+ // const dataTable = new DataTable({
+ // columns: {
+ // name: ['DATATABLE', 'Bob', 'Charlie', 'David', 'Eve'],
+ // age: [23, 34, 45, 56, 67],
+ // city: ['New York', 'Oslo', 'Paris', 'Tokyo', 'London'],
+ // salary: [50000, 60000, 70000, 80000, 90000]
+ // }
+ // });
+
+ // ==== ACTIONS ====
+ const onButtonClick = () => {
+ // console.info('(ref) grid:', grid.current?.grid);
+ setDataSource({
+ name: ['John', 'Jane', 'Jim', 'Jill', 'Jack'],
+ age: [30, 25, 35, 40, 45],
+ city: ['New York', 'Los Angeles', 'Chicago', 'Houston', 'Miami'],
+ salary: [40000, 35000, 45000, 50000, 55000]
+ });
+ };
+
+ const onGridCallback = (grid: GridInstance) => {
+ console.info('(callback) grid:', grid);
+ };
+
+ return (
+ <>
+
+
+
+ Grid Caption v2.1
+
+
+
+
+
+ Grid Description
+ {/* Grid Pagination */}
+
+
+ >
+ );
+}
+
+export default App;
diff --git a/examples/grid-lite/components-react/src/index.css b/examples/grid-lite/components-react/src/index.css
new file mode 100644
index 0000000..04bacd8
--- /dev/null
+++ b/examples/grid-lite/components-react/src/index.css
@@ -0,0 +1,26 @@
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
+ 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
+ sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+#root {
+ width: 100%;
+ min-height: 100vh;
+ padding: 20px;
+}
+
+@media (prefers-color-scheme: dark) {
+ body {
+ background-color: #121212;
+ color: #ffffff;
+ }
+}
diff --git a/examples/grid-lite/components-react/src/main.tsx b/examples/grid-lite/components-react/src/main.tsx
new file mode 100644
index 0000000..0c657b5
--- /dev/null
+++ b/examples/grid-lite/components-react/src/main.tsx
@@ -0,0 +1,11 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import App from './App';
+import './index.css';
+
+ReactDOM.createRoot(document.getElementById('root')!).render(
+
+
+
+);
+
diff --git a/examples/grid-lite/components-react/tsconfig.json b/examples/grid-lite/components-react/tsconfig.json
new file mode 100644
index 0000000..78a6daf
--- /dev/null
+++ b/examples/grid-lite/components-react/tsconfig.json
@@ -0,0 +1,28 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "lib": [
+ "DOM",
+ "ES2016",
+ "ES2017.Object"
+ ],
+ "jsx": "react-jsx",
+ "module": "ES6",
+ "moduleResolution": "node",
+ "esModuleInterop": true,
+ "forceConsistentCasingInFileNames": true,
+ "strict": true,
+ "noImplicitThis": true,
+ "noFallthroughCasesInSwitch": true,
+ "skipDefaultLibCheck": true,
+ "skipLibCheck": true,
+ "ignoreDeprecations": "5.0",
+ "allowSyntheticDefaultImports": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true
+ },
+ "include": ["src"],
+ "references": [{ "path": "./tsconfig.node.json" }]
+}
+
diff --git a/examples/grid-lite/components-react/tsconfig.node.json b/examples/grid-lite/components-react/tsconfig.node.json
new file mode 100644
index 0000000..f7c2070
--- /dev/null
+++ b/examples/grid-lite/components-react/tsconfig.node.json
@@ -0,0 +1,12 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "skipLibCheck": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "allowSyntheticDefaultImports": true,
+ "types": ["node"]
+ },
+ "include": ["vite.config.ts"]
+}
+
diff --git a/examples/grid-lite/components-react/vite.config.ts b/examples/grid-lite/components-react/vite.config.ts
new file mode 100644
index 0000000..2e3f28b
--- /dev/null
+++ b/examples/grid-lite/components-react/vite.config.ts
@@ -0,0 +1,30 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { resolve, dirname } from 'path';
+import { fileURLToPath } from 'url';
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+
+export default defineConfig({
+ plugins: [react()],
+ resolve: {
+ alias: [
+ {
+ find: '@highcharts/grid-lite-react',
+ replacement: resolve(__dirname, '../../../packages/grid-lite-react/src/index.ts')
+ },
+ {
+ find: '@highcharts/grid-shared-react',
+ replacement: resolve(__dirname, '../../../packages/grid-shared-react/src/index.ts')
+ },
+ {
+ find: /^@highcharts\/grid-lite(\/.*)?$/,
+ replacement: resolve(__dirname, 'node_modules/@highcharts/grid-lite$1')
+ }
+ ]
+ },
+ server: {
+ port: 3000
+ }
+});
+
diff --git a/packages/grid-lite-react/src/Grid.tsx b/packages/grid-lite-react/src/Grid.tsx
index f4d21fb..aa47803 100644
--- a/packages/grid-lite-react/src/Grid.tsx
+++ b/packages/grid-lite-react/src/Grid.tsx
@@ -7,14 +7,37 @@
*
*/
+import { useMemo } from 'react';
import {
BaseGrid,
- GridProps
+ GridProps,
+ getChildProps
} from '@highcharts/grid-shared-react';
+import { merge } from '@highcharts/grid-lite/es-modules/Shared/Utilities.js';
import Grid from '@highcharts/grid-lite/es-modules/masters/grid-lite.src';
import '@highcharts/grid-lite/css/grid-lite.css';
import type { Options } from '@highcharts/grid-lite/es-modules/Grid/Core/Options';
-export default function GridLite({ options, gridRef, callback }: GridProps) {
- return ;
+export default function GridLite(props: GridProps) {
+ const { gridRef, children, options, ...gridProps } = props;
+ const childOptions = useMemo(() => getChildProps(children), [children]);
+ const columnKey = useMemo(() => {
+ const columns = childOptions.columns as Array<{ id?: string }> | undefined;
+
+ return columns?.map((column) => column.id).join('\0') ?? '';
+ }, [childOptions]);
+ const gridOptions = useMemo(
+ () => merge(childOptions, options ?? {}) as Options,
+ [childOptions, options]
+ );
+
+ return (
+
+ );
}
diff --git a/packages/grid-lite-react/src/index.ts b/packages/grid-lite-react/src/index.ts
index 93cb0e5..4272e3d 100644
--- a/packages/grid-lite-react/src/index.ts
+++ b/packages/grid-lite-react/src/index.ts
@@ -11,6 +11,21 @@ import GridLite from '@highcharts/grid-lite';
export { default as Grid } from './Grid';
export { default as GridLite } from './Grid';
-export type { GridInstance } from '@highcharts/grid-shared-react';
-export type { GridRefHandle } from '@highcharts/grid-shared-react';
+export { Caption, Data, ColumnDefaults, Column, Description } from '@highcharts/grid-shared-react';
+export { DataTable, DataConnector } from '@highcharts/grid-lite';
+export { merge } from '@highcharts/grid-lite/es-modules/Shared/Utilities.js';
+export type {
+ GridInstance,
+ GridRefHandle,
+ CaptionProps,
+ DescriptionProps,
+ DataProps,
+ DataColumns,
+ DataColumnValue,
+ ColumnProps,
+ ColumnOptionsProps,
+ ColumnDataType,
+ ColumnSortingOrder,
+ CellValueGetterContext
+} from '@highcharts/grid-shared-react';
export type GridOptions = GridLite.Options;
diff --git a/packages/grid-pro-react/src/Grid.tsx b/packages/grid-pro-react/src/Grid.tsx
index 398edea..c236cfd 100644
--- a/packages/grid-pro-react/src/Grid.tsx
+++ b/packages/grid-pro-react/src/Grid.tsx
@@ -7,14 +7,37 @@
*
*/
+import { useMemo } from 'react';
import {
BaseGrid,
- GridProps
+ GridProps,
+ getChildProps
} from '@highcharts/grid-shared-react';
+import { merge } from '@highcharts/grid-pro/es-modules/Shared/Utilities.js';
import Grid from '@highcharts/grid-pro/es-modules/masters/grid-pro.src';
import '@highcharts/grid-pro/css/grid-pro.css';
import type { Options } from '@highcharts/grid-pro/es-modules/Grid/Core/Options';
-export default function GridPro({ options, gridRef, callback }: GridProps) {
- return ;
+export default function GridPro(props: GridProps) {
+ const { gridRef, children, options, ...gridProps } = props;
+ const childOptions = useMemo(() => getChildProps(children), [children]);
+ const columnKey = useMemo(() => {
+ const columns = childOptions.columns as Array<{ id?: string }> | undefined;
+
+ return columns?.map((column) => column.id).join('\0') ?? '';
+ }, [childOptions]);
+ const gridOptions = useMemo(
+ () => merge(childOptions, options ?? {}) as Options,
+ [childOptions, options]
+ );
+
+ return (
+
+ );
}
diff --git a/packages/grid-pro-react/src/index.ts b/packages/grid-pro-react/src/index.ts
index a44be66..a8a0200 100644
--- a/packages/grid-pro-react/src/index.ts
+++ b/packages/grid-pro-react/src/index.ts
@@ -11,6 +11,21 @@ import GridPro from '@highcharts/grid-pro';
export { default as Grid } from './Grid';
export { default as GridPro } from './Grid';
-export type { GridInstance } from '@highcharts/grid-shared-react';
-export type { GridRefHandle } from '@highcharts/grid-shared-react';
+export { Caption, Data, ColumnDefaults, Column, Description } from '@highcharts/grid-shared-react';
+export { DataTable, DataConnector } from '@highcharts/grid-pro';
+export { merge } from '@highcharts/grid-pro/es-modules/Shared/Utilities.js';
+export type {
+ GridInstance,
+ GridRefHandle,
+ CaptionProps,
+ DescriptionProps,
+ DataProps,
+ DataColumns,
+ DataColumnValue,
+ ColumnProps,
+ ColumnOptionsProps,
+ ColumnDataType,
+ ColumnSortingOrder,
+ CellValueGetterContext
+} from '@highcharts/grid-shared-react';
export type GridOptions = GridPro.Options;
diff --git a/packages/grid-shared-react/src/components/BaseGrid.tsx b/packages/grid-shared-react/src/components/BaseGrid.tsx
index 437459e..4d51c20 100644
--- a/packages/grid-shared-react/src/components/BaseGrid.tsx
+++ b/packages/grid-shared-react/src/components/BaseGrid.tsx
@@ -7,7 +7,7 @@
*
*/
-import { useRef, useImperativeHandle, forwardRef, ForwardedRef } from 'react';
+import { useRef, useImperativeHandle, forwardRef, ForwardedRef, ReactNode } from 'react';
import {
useGrid,
GridType,
@@ -31,7 +31,11 @@ export interface GridProps {
/**
* Grid configuration options
*/
- options: TOptions;
+ options?: TOptions;
+ /**
+ * Declarative option components (e.g. Caption) passed as children.
+ */
+ children?: ReactNode;
/**
* Optional ref to access the grid instance
*/
@@ -45,11 +49,10 @@ export interface GridProps {
/**
* Props for BaseGrid component
*/
-export interface BaseGridProps extends GridProps {
- /**
- * Grid instance (from @highcharts/grid-lite or @highcharts/grid-pro)
- */
+export interface BaseGridProps {
+ options?: TOptions;
Grid: GridType;
+ callback?: (grid: GridInstance) => void;
}
export const BaseGrid = forwardRef(function BaseGrid(
diff --git a/packages/grid-shared-react/src/components/BaseGridOptions.ts b/packages/grid-shared-react/src/components/BaseGridOptions.ts
new file mode 100644
index 0000000..9440edc
--- /dev/null
+++ b/packages/grid-shared-react/src/components/BaseGridOptions.ts
@@ -0,0 +1,32 @@
+/**
+ * Grid React integration.
+ * Copyright (c) 2025, Highsoft
+ *
+ * A valid license is required for using this software.
+ * See highcharts.com/license
+ *
+ */
+
+/**
+ * Metadata attached to declarative option components rendered as BaseGrid children.
+ */
+export interface BaseGridOptions {
+ type: 'Grid_Option';
+ /**
+ * Dot-notation path of the Grid option (e.g. `caption`).
+ */
+ gridOption: string;
+ /**
+ * Sub-option that receives string children (e.g. `text`).
+ */
+ childOption?: string;
+ defaultOptions?: Record;
+ isArrayType?: boolean;
+}
+
+/**
+ * A React component that maps JSX props to a Grid options path via `_GridReact`.
+ */
+export interface BaseGridOptionsComponent {
+ _GridReact: BaseGridOptions;
+}
diff --git a/packages/grid-shared-react/src/components/options/caption/Caption.tsx b/packages/grid-shared-react/src/components/options/caption/Caption.tsx
new file mode 100644
index 0000000..99c9199
--- /dev/null
+++ b/packages/grid-shared-react/src/components/options/caption/Caption.tsx
@@ -0,0 +1,33 @@
+/**
+ * Grid React integration.
+ * Copyright (c) 2025, Highsoft
+ *
+ * A valid license is required for using this software.
+ * See highcharts.com/license
+ *
+ */
+
+import { ReactNode } from 'react';
+
+export interface CaptionProps {
+ /**
+ * The custom CSS class name for the table caption.
+ */
+ className?: string;
+ /**
+ * The HTML tag to use for the caption.
+ */
+ htmlTag?: string;
+ children?: ReactNode;
+}
+
+export function Caption(_props: CaptionProps) {
+ return null;
+}
+
+Caption._GridReact = {
+ type: 'Grid_Option',
+ gridOption: 'caption',
+ childOption: 'text',
+ isArrayType: false
+};
diff --git a/packages/grid-shared-react/src/components/options/caption/index.ts b/packages/grid-shared-react/src/components/options/caption/index.ts
new file mode 100644
index 0000000..3e96b78
--- /dev/null
+++ b/packages/grid-shared-react/src/components/options/caption/index.ts
@@ -0,0 +1,11 @@
+/**
+ * Grid React integration.
+ * Copyright (c) 2025, Highsoft
+ *
+ * A valid license is required for using this software.
+ * See highcharts.com/license
+ *
+ */
+
+export { Caption } from './Caption';
+export type { CaptionProps } from './Caption';
diff --git a/packages/grid-shared-react/src/components/options/columns/Column.tsx b/packages/grid-shared-react/src/components/options/columns/Column.tsx
new file mode 100644
index 0000000..f5cd44a
--- /dev/null
+++ b/packages/grid-shared-react/src/components/options/columns/Column.tsx
@@ -0,0 +1,20 @@
+/**
+ * Grid React integration.
+ * Copyright (c) 2025, Highsoft
+ *
+ * A valid license is required for using this software.
+ * See highcharts.com/license
+ *
+ */
+
+import type { ColumnProps } from './columnProps';
+
+export function Column(_props: ColumnProps) {
+ return null;
+}
+
+Column._GridReact = {
+ type: 'Grid_Option',
+ gridOption: 'columns',
+ isArrayType: true
+};
diff --git a/packages/grid-shared-react/src/components/options/columns/ColumnDefaults.tsx b/packages/grid-shared-react/src/components/options/columns/ColumnDefaults.tsx
new file mode 100644
index 0000000..d8569ba
--- /dev/null
+++ b/packages/grid-shared-react/src/components/options/columns/ColumnDefaults.tsx
@@ -0,0 +1,19 @@
+/**
+ * Grid React integration.
+ * Copyright (c) 2025, Highsoft
+ *
+ * A valid license is required for using this software.
+ * See highcharts.com/license
+ *
+ */
+
+import type { ColumnOptionsProps } from './columnProps';
+
+export function ColumnDefaults(_props: ColumnOptionsProps) {
+ return null;
+}
+
+ColumnDefaults._GridReact = {
+ type: 'Grid_Option',
+ gridOption: 'columnDefaults'
+};
diff --git a/packages/grid-shared-react/src/components/options/columns/columnProps.ts b/packages/grid-shared-react/src/components/options/columns/columnProps.ts
new file mode 100644
index 0000000..1452f99
--- /dev/null
+++ b/packages/grid-shared-react/src/components/options/columns/columnProps.ts
@@ -0,0 +1,74 @@
+/**
+ * Grid React integration.
+ * Copyright (c) 2025, Highsoft
+ *
+ * A valid license is required for using this software.
+ * See highcharts.com/license
+ *
+ */
+
+export type ColumnDataType = 'string' | 'number' | 'boolean' | 'datetime';
+
+export type ColumnSortingOrder = 'asc' | 'desc' | null;
+
+/**
+ * `this` context passed to `cellValueGetter` by Grid Core.
+ */
+export interface CellValueGetterContext {
+ row: {
+ index: number;
+ };
+}
+
+/**
+ * Shared column options (`columnDefaults` and per-column overrides).
+ */
+export interface ColumnOptionsProps {
+ dataType?: ColumnDataType;
+ width?: number | string;
+ sortingEnabled?: boolean;
+ sortingOrder?: ColumnSortingOrder;
+ sortingPriority?: number;
+ sortingOrderSequence?: ColumnSortingOrder[];
+ sortingCompare?: (a: unknown, b: unknown) => number;
+ filteringEnabled?: boolean;
+ filteringInline?: boolean;
+ filteringCondition?: string;
+ filteringValue?: string | number | boolean | null;
+ headerClassName?: string;
+ headerFormat?: string;
+ headerFormatter?: (this: unknown) => string;
+ headerStyle?: unknown;
+ cellRowHeader?: boolean;
+ cellClassName?: string;
+ cellFormat?: string;
+ cellFormatter?: (this: unknown) => string;
+ /**
+ * Custom cell value resolver. `this` is the Grid table cell (`row.index`
+ * is the row index in the presentation data).
+ */
+ cellValueGetter?: (this: CellValueGetterContext) => unknown;
+ cellContextMenu?: {
+ enabled?: boolean;
+ items?: unknown[];
+ };
+ cellStyle?: unknown;
+ style?: unknown;
+ exportable?: boolean;
+}
+
+export interface ColumnProps extends ColumnOptionsProps {
+ /**
+ * HTML `id` attribute for styling hooks. Not passed to Grid options.
+ */
+ id?: string;
+ /**
+ * References the column to configure (data field id). Maps header, cells,
+ * sorting, filtering, etc. to Grid Core column options.
+ *
+ * Becomes `options.columns[].id` in Grid Core (same identifier).
+ */
+ columnId?: string;
+ className?: string;
+ enabled?: boolean;
+}
diff --git a/packages/grid-shared-react/src/components/options/data/Data.tsx b/packages/grid-shared-react/src/components/options/data/Data.tsx
new file mode 100644
index 0000000..7ec764a
--- /dev/null
+++ b/packages/grid-shared-react/src/components/options/data/Data.tsx
@@ -0,0 +1,64 @@
+/**
+ * Grid React integration.
+ * Copyright (c) 2025, Highsoft
+ *
+ * A valid license is required for using this software.
+ * See highcharts.com/license
+ *
+ */
+
+export type DataColumnValue = boolean | null | number | string | undefined;
+
+export type DataColumns = Record>;
+
+export interface DataProps {
+ /**
+ * The type of the data provider.
+ *
+ * @default 'local'
+ */
+ providerType?: 'local' | string;
+ /**
+ * Whether columns should be generated automatically from data source
+ * column ids.
+ *
+ * Defaults to `true`. When declarative `` components are used,
+ * the React wrapper sets this to `false` unless you pass this prop
+ * explicitly.
+ *
+ * @default true
+ */
+ autogenerateColumns?: boolean;
+ /**
+ * Columns data to initialize the Grid with.
+ */
+ columns?: DataColumns;
+ /**
+ * Data table as a source of data for the grid.
+ */
+ dataTable?: unknown;
+ /**
+ * Connector instance or options used to populate the data table.
+ */
+ connector?: unknown;
+ /**
+ * Automatically update the grid when the data table changes.
+ *
+ * @default false
+ */
+ updateOnChange?: boolean;
+ /**
+ * The column ID that contains the stable, unique row IDs.
+ */
+ idColumn?: string;
+}
+
+export function Data(_props: DataProps) {
+ return null;
+}
+
+Data._GridReact = {
+ type: 'Grid_Option',
+ gridOption: 'data',
+ isArrayType: false
+};
diff --git a/packages/grid-shared-react/src/components/options/description/Description.tsx b/packages/grid-shared-react/src/components/options/description/Description.tsx
new file mode 100644
index 0000000..c6ee575
--- /dev/null
+++ b/packages/grid-shared-react/src/components/options/description/Description.tsx
@@ -0,0 +1,29 @@
+/**
+ * Grid React integration.
+ * Copyright (c) 2025, Highsoft
+ *
+ * A valid license is required for using this software.
+ * See highcharts.com/license
+ *
+ */
+
+import { ReactNode } from 'react';
+
+export interface DescriptionProps {
+ /**
+ * The custom CSS class name for the description.
+ */
+ className?: string;
+ children?: ReactNode;
+}
+
+export function Description(_props: DescriptionProps) {
+ return null;
+}
+
+Description._GridReact = {
+ type: 'Grid_Option',
+ gridOption: 'description',
+ childOption: 'text',
+ isArrayType: false
+};
diff --git a/packages/grid-shared-react/src/components/options/description/index.ts b/packages/grid-shared-react/src/components/options/description/index.ts
new file mode 100644
index 0000000..65cb487
--- /dev/null
+++ b/packages/grid-shared-react/src/components/options/description/index.ts
@@ -0,0 +1,11 @@
+/**
+ * Grid React integration.
+ * Copyright (c) 2025, Highsoft
+ *
+ * A valid license is required for using this software.
+ * See highcharts.com/license
+ *
+ */
+
+export { Description } from './Description';
+export type { DescriptionProps } from './Description';
diff --git a/packages/grid-shared-react/src/components/options/index.ts b/packages/grid-shared-react/src/components/options/index.ts
new file mode 100644
index 0000000..dbbf7f4
--- /dev/null
+++ b/packages/grid-shared-react/src/components/options/index.ts
@@ -0,0 +1,24 @@
+/**
+ * Grid React integration.
+ * Copyright (c) 2025, Highsoft
+ *
+ * A valid license is required for using this software.
+ * See highcharts.com/license
+ *
+ */
+
+export { Caption } from './caption';
+export type { CaptionProps } from './caption';
+export { Data } from './data/Data';
+export type { DataProps, DataColumns, DataColumnValue } from './data/Data';
+export { ColumnDefaults } from './columns/ColumnDefaults';
+export { Column } from './columns/Column';
+export type {
+ ColumnProps,
+ ColumnOptionsProps,
+ ColumnDataType,
+ ColumnSortingOrder,
+ CellValueGetterContext
+} from './columns/columnProps';
+export { Description } from './description';
+export type { DescriptionProps } from './description';
diff --git a/packages/grid-shared-react/src/hooks/useGrid.test.tsx b/packages/grid-shared-react/src/hooks/useGrid.test.tsx
index 774a6a1..53e86d1 100644
--- a/packages/grid-shared-react/src/hooks/useGrid.test.tsx
+++ b/packages/grid-shared-react/src/hooks/useGrid.test.tsx
@@ -60,9 +60,10 @@ describe('useGrid', () => {
expect(initQueue).toHaveLength(1);
});
- const [firstInit] = initQueue;
+ const firstInit = initQueue[0];
- await firstInit.resolve();
+ expect(firstInit).toBeDefined();
+ await firstInit!.resolve();
await waitFor(() => {
expect(container.querySelector('[data-grid-id="1"]')).not.toBeNull();
diff --git a/packages/grid-shared-react/src/hooks/useGrid.ts b/packages/grid-shared-react/src/hooks/useGrid.ts
index 327e3fb..c5b752f 100644
--- a/packages/grid-shared-react/src/hooks/useGrid.ts
+++ b/packages/grid-shared-react/src/hooks/useGrid.ts
@@ -8,14 +8,13 @@
*/
import { useEffect, RefObject, useRef } from 'react';
-import { BaseGridProps } from '../components/BaseGrid';
/**
* Interface describing the shape of a Grid instance returned by Grid.grid()
*/
export interface GridInstance {
destroy(): void;
- update(options: TOptions, redraw?: boolean): void;
+ update(options: TOptions, redraw?: boolean, oneToOne?: boolean): void;
}
/**
@@ -25,11 +24,14 @@ export interface GridInstance {
* directly depending on their types.
*/
export interface GridType {
- grid(container: HTMLDivElement, options: TOptions, async?: boolean): GridInstance | Promise>;
+ grid(container: HTMLDivElement, options?: TOptions, async?: boolean): GridInstance | Promise>;
}
-export interface UseGridOptions extends BaseGridProps {
+export interface UseGridOptions {
containerRef: RefObject;
+ options?: TOptions;
+ Grid: GridType;
+ callback?: (grid: GridInstance) => void;
}
export function useGrid({
@@ -40,7 +42,7 @@ export function useGrid({
}: UseGridOptions) {
const currGridRef = useRef | null>(null);
const callbackRef = useRef(callback);
- const pendingOptionsRef = useRef(null);
+ const pendingOptionsRef = useRef(void 0);
const initStartedRef = useRef(false);
// StrictMode runs effects twice: mount → cleanup → mount.
@@ -73,7 +75,7 @@ export function useGrid({
try {
// Use pending options if available (from rapid updates during init)
const initOptions = pendingOptionsRef.current ?? options;
- pendingOptionsRef.current = null;
+ pendingOptionsRef.current = void 0;
const grid = await Grid.grid(container, initOptions, true);
@@ -86,9 +88,9 @@ export function useGrid({
currGridRef.current = grid;
// Apply any pending options that came in while we were initializing
- if (pendingOptionsRef.current) {
- grid.update(pendingOptionsRef.current, true);
- pendingOptionsRef.current = null;
+ if (pendingOptionsRef.current !== void 0) {
+ grid.update(pendingOptionsRef.current, true, true);
+ pendingOptionsRef.current = void 0;
}
callbackRef.current?.(grid);
@@ -115,9 +117,13 @@ export function useGrid({
// Effect for options updates - separate from init
useEffect(() => {
+ if (options === void 0) {
+ return;
+ }
+
if (currGridRef.current) {
- // Grid exists, update it directly
- currGridRef.current.update(options, true);
+ // Declarative React options replace the previous snapshot (oneToOne).
+ currGridRef.current.update(options, true, true);
} else {
// Grid still initializing, queue the update
pendingOptionsRef.current = options;
diff --git a/packages/grid-shared-react/src/index.ts b/packages/grid-shared-react/src/index.ts
index 6a47b1b..3814da0 100644
--- a/packages/grid-shared-react/src/index.ts
+++ b/packages/grid-shared-react/src/index.ts
@@ -12,4 +12,18 @@ import { GridType, GridInstance } from './hooks/useGrid';
import type { GridProps, GridRefHandle } from './components/BaseGrid';
export { BaseGrid };
+export { Caption, Data, ColumnDefaults, Column, Description } from './components/options';
+export { getChildProps } from './utils/getChildProps';
+export type {
+ CaptionProps,
+ DescriptionProps,
+ DataProps,
+ DataColumns,
+ DataColumnValue,
+ ColumnProps,
+ ColumnOptionsProps,
+ ColumnDataType,
+ ColumnSortingOrder,
+ CellValueGetterContext
+} from './components/options';
export type { GridType, GridInstance, GridProps, GridRefHandle };
diff --git a/packages/grid-shared-react/src/utils/getChildProps.test.tsx b/packages/grid-shared-react/src/utils/getChildProps.test.tsx
new file mode 100644
index 0000000..1498a4e
--- /dev/null
+++ b/packages/grid-shared-react/src/utils/getChildProps.test.tsx
@@ -0,0 +1,50 @@
+import { describe, it, expect } from 'vitest';
+import { Data } from '../components/options/data/Data';
+import { getChildProps } from './getChildProps';
+
+describe('getChildProps', () => {
+ it('maps Data columns to options.data.columns', () => {
+ const columns = {
+ name: ['Alice', 'Bob'],
+ age: [23, 34]
+ };
+
+ expect(getChildProps()).toEqual({
+ data: {
+ columns
+ }
+ });
+ });
+
+ it('maps all Data props to options.data', () => {
+ const columns = {
+ name: ['Alice']
+ };
+ const connector = { id: 'csv' };
+ const dataTable = { id: 'table-1' };
+
+ expect(
+ getChildProps(
+
+ )
+ ).toEqual({
+ data: {
+ providerType: 'local',
+ autogenerateColumns: false,
+ columns,
+ connector,
+ dataTable,
+ updateOnChange: true,
+ idColumn: 'id'
+ }
+ });
+ });
+});
diff --git a/packages/grid-shared-react/src/utils/getChildProps.ts b/packages/grid-shared-react/src/utils/getChildProps.ts
new file mode 100644
index 0000000..572376d
--- /dev/null
+++ b/packages/grid-shared-react/src/utils/getChildProps.ts
@@ -0,0 +1,291 @@
+/**
+ * Grid React integration.
+ * Copyright (c) 2025, Highsoft
+ *
+ * A valid license is required for using this software.
+ * See highcharts.com/license
+ *
+ */
+
+import { Fragment, isValidElement, ReactElement, ReactNode } from 'react';
+import type { BaseGridOptionsComponent, BaseGridOptions } from '../components/BaseGridOptions';
+import { normalizeColumnOptions } from './mappers/columnOptions';
+
+function objInsert(
+ obj: Record,
+ path: string,
+ value: unknown
+): Record {
+ const keys = path.split('.');
+ let current = obj;
+
+ for (let i = 0; i < keys.length - 1; i++) {
+ const key = keys[i];
+
+ if (key === void 0) {
+ continue;
+ }
+
+ if (!isObject(current[key])) {
+ current[key] = {};
+ }
+ current = current[key] as Record;
+ }
+
+ const lastKey = keys.at(-1);
+
+ if (lastKey !== void 0) {
+ current[lastKey] = value;
+ }
+ return obj;
+}
+
+function isObject(value: unknown): value is Record {
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
+}
+
+function isReactElement(value: unknown): value is ReactElement {
+ return isValidElement(value);
+}
+
+function getOptionComponent(type: unknown): BaseGridOptionsComponent | null {
+ if (typeof type !== 'function' && (typeof type !== 'object' || type === null)) {
+ return null;
+ }
+
+ const component = type as Partial;
+
+ return component._GridReact ? type as BaseGridOptionsComponent : null;
+}
+
+function getChildPropsFromElement(child: ReactElement): Record {
+ return (child.props ?? {}) as Record;
+}
+
+function renderChildren(children: ReactNode): string {
+ if (typeof children === 'string' || typeof children === 'number') {
+ return String(children);
+ }
+
+ if (Array.isArray(children)) {
+ return children
+ .map((child) => renderChildren(child))
+ .join('');
+ }
+
+ return '';
+}
+
+function flattenChildren(childNodes: ReactNode): ReactNode[] {
+ if (childNodes == null || childNodes === false) {
+ return [];
+ }
+
+ if (Array.isArray(childNodes)) {
+ return childNodes.flatMap((child) => flattenChildren(child));
+ }
+
+ if (isReactElement(childNodes) && childNodes.type === Fragment) {
+ return flattenChildren((childNodes.props as { children?: ReactNode }).children);
+ }
+
+ return [childNodes];
+}
+
+function getEffectiveMeta(
+ component: BaseGridOptionsComponent,
+ parentMeta?: BaseGridOptions
+): BaseGridOptions {
+ const meta = component._GridReact;
+
+ if (!parentMeta) {
+ return meta;
+ }
+
+ return {
+ ...meta,
+ childOption: parentMeta.childOption
+ ? `${parentMeta.childOption}.${meta.childOption ?? ''}`
+ : meta.childOption,
+ gridOption: parentMeta.gridOption
+ ? `${parentMeta.gridOption}.${meta.gridOption}`
+ : meta.gridOption
+ };
+}
+
+function parseColumnElement(child: ReactElement): Record {
+ const { children: _ignored, columnId, id: _cssId, ...props } = getChildPropsFromElement(child);
+ const options = normalizeColumnOptions(props);
+
+ // columnId selects the column; Core expects the same value as `id`.
+ if (columnId !== void 0) {
+ options.id = columnId;
+ }
+
+ return options;
+}
+
+function pushColumn(
+ optionsFromChildren: Record,
+ child: ReactElement
+): void {
+ const columns = (optionsFromChildren.columns ?? (
+ optionsFromChildren.columns = []
+ )) as Record[];
+
+ columns.push(parseColumnElement(child));
+}
+
+export function getChildProps(children: ReactNode): Record {
+ const optionsFromChildren: Record = {};
+ const resolvedChildren = flattenChildren(children)
+ .map((child) => resolveOptionChild(child))
+ .filter((child): child is ReactElement => child !== null);
+
+ function handleChildren(
+ childNodes: ReactNode,
+ obj: Record,
+ meta: BaseGridOptions
+ ): void {
+ if (childNodes == null || childNodes === false) {
+ return;
+ }
+
+ const nonOptionChildren: ReactNode[] = [];
+
+ if (Array.isArray(childNodes)) {
+ for (const child of childNodes) {
+ if (isReactElement(child) && isOptionElement(child)) {
+ handleChild(child, meta);
+ continue;
+ }
+
+ nonOptionChildren.push(child);
+ }
+ } else if (isReactElement(childNodes) && isOptionElement(childNodes)) {
+ handleChild(childNodes, meta);
+ } else {
+ nonOptionChildren.push(childNodes);
+ }
+
+ if (meta.childOption) {
+ const childrenToRender = nonOptionChildren.length > 0 ?
+ nonOptionChildren :
+ [childNodes];
+
+ objInsert(obj, meta.childOption, renderChildren(childrenToRender));
+ }
+ }
+
+ function handleChild(child: ReactElement, parentMeta?: BaseGridOptions): void {
+ const component = getOptionComponent(child.type);
+
+ if (!component) {
+ return;
+ }
+
+ const meta = getEffectiveMeta(component, parentMeta);
+
+ if (!meta.gridOption) {
+ return;
+ }
+
+ const childProps = getChildPropsFromElement(child);
+ const { children: childChildren, ...props } = childProps;
+
+ if (meta.gridOption === 'columnDefaults') {
+ optionsFromChildren.columnDefaults = normalizeColumnOptions(props);
+ return;
+ }
+
+ if (meta.gridOption === 'columns') {
+ pushColumn(optionsFromChildren, child);
+ return;
+ }
+
+ const optionParent = optionsFromChildren[meta.gridOption] ?? (
+ optionsFromChildren[meta.gridOption] = meta.isArrayType ? [] : {}
+ );
+ const parentIsArray = Array.isArray(optionParent);
+ const insertInto = parentIsArray ? {} : optionParent as Record;
+
+ if (meta.defaultOptions) {
+ Object.assign(insertInto, meta.defaultOptions);
+ }
+
+ Object.assign(insertInto, props);
+
+ if (typeof childChildren === 'string' || typeof childChildren === 'number') {
+ if (meta.childOption) {
+ objInsert(insertInto, meta.childOption, String(childChildren));
+ }
+ } else if (childChildren != null) {
+ handleChildren(childChildren as ReactNode, insertInto, meta);
+ }
+
+ if (parentIsArray) {
+ (optionsFromChildren[meta.gridOption] as unknown[]).push(insertInto);
+ }
+ }
+
+ for (const child of resolvedChildren) {
+ handleChild(child);
+ }
+
+ applyDeclarativeColumnDefaults(optionsFromChildren);
+
+ return optionsFromChildren;
+}
+
+/**
+ * When declarative `` components are present, only those columns
+ * should render unless `data.autogenerateColumns` is set explicitly on ``.
+ */
+function applyDeclarativeColumnDefaults(
+ optionsFromChildren: Record
+): void {
+ const columns = optionsFromChildren.columns;
+
+ if (!Array.isArray(columns) || columns.length === 0) {
+ return;
+ }
+
+ const data = isObject(optionsFromChildren.data) ?
+ { ...optionsFromChildren.data } :
+ {};
+
+ if (!('autogenerateColumns' in data)) {
+ data.autogenerateColumns = false;
+ optionsFromChildren.data = data;
+ }
+}
+
+function isOptionElement(child: ReactElement): boolean {
+ return getOptionComponent(child.type) !== null;
+}
+
+function resolveOptionChild(child: ReactNode): ReactElement | null {
+ if (!isReactElement(child)) {
+ return null;
+ }
+
+ const component = getOptionComponent(child.type);
+
+ if (component) {
+ return child;
+ }
+
+ if (typeof child.type !== 'function') {
+ return null;
+ }
+
+ const rendered = (child.type as (props: Record) => ReactNode)(
+ getChildPropsFromElement(child)
+ );
+
+ if (isReactElement(rendered) && getOptionComponent(rendered.type)) {
+ return rendered;
+ }
+
+ return null;
+}
diff --git a/packages/grid-shared-react/src/utils/mappers/columnOptions.ts b/packages/grid-shared-react/src/utils/mappers/columnOptions.ts
new file mode 100644
index 0000000..300c39c
--- /dev/null
+++ b/packages/grid-shared-react/src/utils/mappers/columnOptions.ts
@@ -0,0 +1,24 @@
+/**
+ * Grid React integration.
+ * Copyright (c) 2025, Highsoft
+ *
+ * A valid license is required for using this software.
+ * See highcharts.com/license
+ *
+ */
+
+import { mapPrefixedProps } from './mapPrefixedProps';
+
+/** Flat prop prefix → nested Grid option key for columns. */
+const COLUMN_PROP_PREFIXES = {
+ sorting: 'sorting',
+ filtering: 'filtering',
+ header: 'header',
+ cell: 'cells'
+} as const;
+
+export function normalizeColumnOptions(
+ props: Record
+): Record {
+ return mapPrefixedProps(props, COLUMN_PROP_PREFIXES);
+}
diff --git a/packages/grid-shared-react/src/utils/mappers/mapPrefixedProps.ts b/packages/grid-shared-react/src/utils/mappers/mapPrefixedProps.ts
new file mode 100644
index 0000000..9fdd271
--- /dev/null
+++ b/packages/grid-shared-react/src/utils/mappers/mapPrefixedProps.ts
@@ -0,0 +1,57 @@
+/**
+ * Grid React integration.
+ * Copyright (c) 2025, Highsoft
+ *
+ * A valid license is required for using this software.
+ * See highcharts.com/license
+ *
+ */
+
+/**
+ * Maps flat props with a shared prefix into nested Grid option objects.
+ *
+ * Convention: `{prefix}{OptionKey}` → `{groupKey}.{optionKey}`
+ *
+ * @example
+ * mapPrefixedProps(
+ * { sortingEnabled: true, sortingOrder: 'asc', width: 120 },
+ * { sorting: 'sorting' }
+ * );
+ * // => { width: 120, sorting: { enabled: true, order: 'asc' } }
+ */
+export type PrefixedPropMap = Record;
+
+export function mapPrefixedProps(
+ props: Record,
+ prefixToGroup: PrefixedPropMap
+): Record {
+ const result = { ...props };
+ const groups: Record> = {};
+ const prefixes = Object.keys(prefixToGroup).sort((a, b) => b.length - a.length);
+
+ for (const flatKey of Object.keys(result)) {
+ const prefix = prefixes.find(
+ (candidate) => flatKey.startsWith(candidate) && flatKey.length > candidate.length
+ );
+
+ if (!prefix) {
+ continue;
+ }
+
+ const groupKey = prefixToGroup[prefix];
+ const nestedKey = toNestedKey(flatKey.slice(prefix.length));
+
+ (groups[groupKey] ??= {})[nestedKey] = result[flatKey];
+ delete result[flatKey];
+ }
+
+ for (const [groupKey, nested] of Object.entries(groups)) {
+ result[groupKey] = nested;
+ }
+
+ return result;
+}
+
+function toNestedKey(segment: string): string {
+ return segment.charAt(0).toLowerCase() + segment.slice(1);
+}
diff --git a/packages/grid-shared-react/tsconfig.json b/packages/grid-shared-react/tsconfig.json
index b1b3107..f1d16c9 100644
--- a/packages/grid-shared-react/tsconfig.json
+++ b/packages/grid-shared-react/tsconfig.json
@@ -1,4 +1,4 @@
{
"extends": "../../tsconfig.base.json",
- "include": ["src", "tests"]
+ "include": ["src"]
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 05e60b9..ff81d18 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -48,6 +48,37 @@ importers:
specifier: ^4.0.16
version: 4.0.16(@types/node@20.19.26)(@vitest/browser-playwright@4.0.16)(jsdom@27.4.0)
+ examples/grid-lite/components-react:
+ dependencies:
+ '@highcharts/grid-lite':
+ specifier: '>=3.0.0'
+ version: 3.0.0
+ '@highcharts/grid-lite-react':
+ specifier: workspace:*
+ version: link:../../../packages/grid-lite-react
+ react:
+ specifier: '>=18'
+ version: 19.2.1
+ react-dom:
+ specifier: '>=18'
+ version: 19.2.1(react@19.2.1)
+ devDependencies:
+ '@types/react':
+ specifier: '>=18'
+ version: 19.2.7
+ '@types/react-dom':
+ specifier: '>=18'
+ version: 19.2.3(@types/react@19.2.7)
+ '@vitejs/plugin-react':
+ specifier: ^4.2.0
+ version: 4.7.0(vite@5.4.21(@types/node@20.19.26))
+ typescript:
+ specifier: ^5.0.0
+ version: 5.9.3
+ vite:
+ specifier: ^5.0.0
+ version: 5.4.21(@types/node@20.19.26)
+
examples/grid-lite/minimal-nextjs:
dependencies:
'@highcharts/grid-lite':