Skip to content

Maintain the data and configuration options of your grid by using the Redux state container.

Integrate with Redux

The following example implements the @handsontable/react-wrapper component with a readOnly toggle switch and the Redux state manager.

Simple example

JavaScript
import { useRef } from 'react';
import { createStore } from 'redux';
import { Provider, useSelector, useDispatch } from 'react-redux';
import { HotTable } from '@handsontable/react-wrapper';
import { registerAllModules } from 'handsontable/registry';
// register Handsontable's modules
registerAllModules();
const ExampleComponentContent = () => {
const hotSettings = useSelector((state) => state);
const dispatch = useDispatch();
const hotTableComponentRef = useRef(null);
const hotData = hotSettings.data;
const isHotData = Array.isArray(hotData);
const onBeforeHotChange = (changes) => {
dispatch({
type: 'updateData',
dataChanges: changes,
});
return false;
};
const toggleReadOnly = (event) => {
dispatch({
type: 'updateReadOnly',
readOnly: event.target.checked,
});
};
return (
<div className="dump-example-container">
<div id="example-container">
<div id="example-preview">
<div className="controls">
<label>
<input onClick={toggleReadOnly} type="checkbox" />
Toggle <code>readOnly</code> for the entire table
</label>
</div>
<HotTable
ref={hotTableComponentRef}
beforeChange={onBeforeHotChange}
autoWrapRow={true}
autoWrapCol={true}
{...hotSettings}
/>
</div>
<h3>Redux store dump</h3>
<pre id="redux-preview" className="table-container">
{isHotData && (
<div>
<strong>data:</strong>
<table style={{ border: '1px solid #d6d6d6' }}>
<tbody>
{hotData.map((row, i) => (
<tr key={i}>
{row.map((cell, i) => (
<td key={i}>{cell}</td>
))}
</tr>
))}
</tbody>
</table>
</div>
)}
<table>
<tbody>
{Object.entries(hotSettings).map(
([name, value]) =>
name !== 'data' && (
<tr key={`${name}${value}`}>
<td>
<strong>{name}:</strong>
</td>
<td>{value.toString()}</td>
</tr>
)
)}
</tbody>
</table>
</pre>
</div>
</div>
);
};
const initialReduxStoreState = {
data: [
['A1', 'B1', 'C1'],
['A2', 'B2', 'C2'],
['A3', 'B3', 'C3'],
['A4', 'B4', 'C4'],
['A5', 'B5', 'C5'],
],
colHeaders: true,
rowHeaders: true,
readOnly: false,
height: 'auto',
licenseKey: 'non-commercial-and-evaluation',
};
const updatesReducer = (state = initialReduxStoreState, action) => {
switch (action.type) {
case 'updateData':
const newData = [...state.data];
action.dataChanges.forEach(([row, column, oldValue, newValue]) => {
newData[row][column] = newValue;
});
return {
...state,
data: newData,
};
case 'updateReadOnly':
return {
...state,
readOnly: action.readOnly,
};
default:
return state;
}
};
const reduxStore = createStore(updatesReducer);
const ExampleComponent = () => (
<Provider store={reduxStore}>
<ExampleComponentContent />
</Provider>
);
export default ExampleComponent;
TypeScript
import { useRef, MouseEvent, JSXElementConstructor, Key, ReactElement, ReactNode, ReactPortal } from 'react';
import { createStore } from 'redux';
import { Provider, useSelector, useDispatch } from 'react-redux';
import { HotTable } from '@handsontable/react-wrapper';
import { registerAllModules } from 'handsontable/registry';
import Handsontable from 'handsontable/base';
// register Handsontable's modules
registerAllModules();
const ExampleComponentContent = () => {
const hotSettings = useSelector((state: RootState) => state);
const dispatch = useDispatch();
const hotTableComponentRef = useRef(null);
const hotData = hotSettings.data;
const isHotData = Array.isArray(hotData);
const onBeforeHotChange = (changes: (Handsontable.CellChange | null)[]) => {
dispatch({
type: 'updateData',
dataChanges: changes,
});
return false;
};
const toggleReadOnly = (event: MouseEvent) => {
dispatch({
type: 'updateReadOnly',
readOnly: (event.target as HTMLInputElement).checked,
});
};
return (
<div className="dump-example-container">
<div id="example-container">
<div id="example-preview">
<div className="controls">
<label>
<input onClick={toggleReadOnly} type="checkbox" />
Toggle <code>readOnly</code> for the entire table
</label>
</div>
<HotTable
ref={hotTableComponentRef}
beforeChange={onBeforeHotChange}
autoWrapRow={true}
autoWrapCol={true}
{...hotSettings}
/>
</div>
<h3>Redux store dump</h3>
<pre id="redux-preview" className="table-container">
{isHotData && (
<div>
<strong>data:</strong>
<table style={{ border: '1px solid #d6d6d6' }}>
<tbody>
{hotData.map((row, i) => (
<tr key={i}>
{row.map(
(
cell:
| string
| number
| boolean
| ReactElement<any, string | JSXElementConstructor<any>>
| Iterable<ReactNode>
| ReactPortal
| null
| undefined,
i: Key | null | undefined
) => (
<td key={i}>{cell}</td>
)
)}
</tr>
))}
</tbody>
</table>
</div>
)}
<table>
<tbody>
{Object.entries(hotSettings).map(
([name, value]) =>
name !== 'data' && (
<tr key={`${name}${value}`}>
<td>
<strong>{name}:</strong>
</td>
<td>{value.toString()}</td>
</tr>
)
)}
</tbody>
</table>
</pre>
</div>
</div>
);
};
const initialReduxStoreState = {
data: [
['A1', 'B1', 'C1'],
['A2', 'B2', 'C2'],
['A3', 'B3', 'C3'],
['A4', 'B4', 'C4'],
['A5', 'B5', 'C5'],
],
colHeaders: true,
rowHeaders: true,
readOnly: false,
height: 'auto',
licenseKey: 'non-commercial-and-evaluation',
};
const updatesReducer = (
state = initialReduxStoreState,
action: { type: any; dataChanges: [any, any, any, any][]; readOnly: any }
) => {
switch (action.type) {
case 'updateData':
const newData = [...state.data];
action.dataChanges.forEach(([row, column, oldValue, newValue]) => {
newData[row][column] = newValue;
});
return {
...state,
data: newData,
};
case 'updateReadOnly':
return {
...state,
readOnly: action.readOnly,
};
default:
return state;
}
};
export type RootState = ReturnType<typeof updatesReducer>;
const reduxStore = createStore(updatesReducer);
const ExampleComponent = () => (
<Provider store={reduxStore}>
<ExampleComponentContent />
</Provider>
);
export default ExampleComponent;

Advanced example

This example shows:

  • A custom editor component (built with an external dependency, HexColorPicker). This component acts both as an editor and as a renderer.
  • A custom renderer component, built with an external dependency (StarRatingComponent).

The editor component changes the behavior of the renderer component, by passing information through Redux (and the connect() method of react-redux).

JavaScript
import { useEffect, useRef, useState } from 'react';
import { HexColorPicker } from 'react-colorful';
import StarRatingComponent from 'react-star-rating-component';
import { Provider, connect, useDispatch } from 'react-redux';
import { createStore, combineReducers } from 'redux';
import { HotTable, HotColumn, useHotEditor } from '@handsontable/react-wrapper';
import { registerAllModules } from 'handsontable/registry';
// register Handsontable's modules
registerAllModules();
const UnconnectedColorPickerEditor = () => {
const dispatch = useDispatch();
const editorRef = useRef(null);
const [pickedColor, setPickedColor] = useState('');
const { value, setValue, isOpen, finishEditing, col, row } = useHotEditor({
onOpen: () => {
if (editorRef.current) editorRef.current.style.display = 'block';
document.querySelector('.react-colorful__interactive')?.focus();
},
onClose: () => {
if (editorRef.current) editorRef.current.style.display = 'none';
setPickedColor('');
},
onPrepare: (_row, _column, _prop, TD, _originalValue, _cellProperties) => {
const tdPosition = TD.getBoundingClientRect();
if (!editorRef.current) return;
editorRef.current.style.left = `${tdPosition.left + window.pageXOffset}px`;
editorRef.current.style.top = `${tdPosition.top + window.pageYOffset}px`;
},
onFocus: () => {},
});
const onPickedColor = (color) => {
setValue(color);
};
const applyColor = () => {
if (col === 1) {
dispatch({
type: 'updateActiveStarColor',
row,
hexColor: value,
});
} else if (col === 2) {
dispatch({
type: 'updateInactiveStarColor',
row,
hexColor: value,
});
}
finishEditing();
};
const stopMousedownPropagation = (e) => {
e.stopPropagation();
};
const stopKeyboardPropagation = (e) => {
e.stopPropagation();
if (e.key === 'Escape') {
applyColor();
}
};
return (
<div
style={{
display: 'none',
position: 'absolute',
left: 0,
top: 0,
zIndex: 999,
background: '#fff',
padding: '15px',
border: '1px solid #cecece',
}}
ref={editorRef}
onMouseDown={stopMousedownPropagation}
onKeyDown={stopKeyboardPropagation}
>
<HexColorPicker color={pickedColor || value} onChange={onPickedColor} />
<button style={{ width: '100%', height: '33px', marginTop: '10px' }} onClick={applyColor}>
Apply
</button>
</div>
);
};
const ColorPickerEditor = connect(function (state) {
return {
activeColors: state.appReducer.activeColors,
inactiveColors: state.appReducer.inactiveColors,
};
})(UnconnectedColorPickerEditor);
const ColorPickerRenderer = ({ value }) => {
return (
<>
<div
style={{
background: value,
width: '21px',
height: '21px',
float: 'left',
marginRight: '5px',
}}
/>
<div>{value}</div>
</>
);
};
// a Redux component
const initialReduxStoreState = {
activeColors: [],
inactiveColors: [],
};
const appReducer = (state = initialReduxStoreState, action) => {
switch (action.type) {
case 'initRatingColors': {
const { hotData } = action;
const activeColors = hotData.map((data) => data[1]);
const inactiveColors = hotData.map((data) => data[2]);
return {
...state,
activeColors,
inactiveColors,
};
}
case 'updateActiveStarColor': {
const rowIndex = action.row;
const newColor = action.hexColor;
const activeColorArray = state.activeColors ? [...state.activeColors] : [];
activeColorArray[rowIndex] = newColor;
return {
...state,
activeColors: activeColorArray,
};
}
case 'updateInactiveStarColor': {
const rowIndex = action.row;
const newColor = action.hexColor;
const inactiveColorArray = state.inactiveColors ? [...state.inactiveColors] : [];
inactiveColorArray[rowIndex] = newColor;
return {
...state,
inactiveColors: inactiveColorArray,
};
}
default:
return state;
}
};
const actionReducers = combineReducers({ appReducer });
const reduxStore = createStore(actionReducers);
// a custom renderer component
const UnconnectedStarRatingRenderer = ({ row, col, value, activeColors, inactiveColors }) => {
return (
<StarRatingComponent
name={`${row}-${col}`}
value={value}
starCount={5}
starColor={activeColors?.[row || 0]}
emptyStarColor={inactiveColors?.[row || 0]}
editing={true}
/>
);
};
const StarRatingRenderer = connect((state) => ({
activeColors: state.appReducer.activeColors,
inactiveColors: state.appReducer.inactiveColors,
}))(UnconnectedStarRatingRenderer);
const data = [
[1, '#ff6900', '#fcb900'],
[2, '#fcb900', '#7bdcb5'],
[3, '#7bdcb5', '#8ed1fc'],
[4, '#00d084', '#0693e3'],
[5, '#eb144c', '#abb8c3'],
];
const ExampleComponent = () => {
useEffect(() => {
reduxStore.dispatch({
type: 'initRatingColors',
hotData: data,
});
}, []);
return (
<Provider store={reduxStore}>
<HotTable
data={data}
rowHeaders={true}
rowHeights={30}
colHeaders={['Rating', 'Active star color', 'Inactive star color']}
height="auto"
autoWrapRow={true}
autoWrapCol={true}
licenseKey="non-commercial-and-evaluation"
>
<HotColumn width={100} type="numeric" renderer={StarRatingRenderer} />
<HotColumn width={150} renderer={ColorPickerRenderer} editor={ColorPickerEditor} />
<HotColumn width={150} renderer={ColorPickerRenderer} editor={ColorPickerEditor} />
</HotTable>
</Provider>
);
};
export default ExampleComponent;
TypeScript
import { useEffect, MouseEvent, KeyboardEvent, useRef, useState } from 'react';
import Handsontable from 'handsontable/base';
import { HexColorPicker } from 'react-colorful';
import StarRatingComponent from 'react-star-rating-component';
import { Provider, connect, useDispatch } from 'react-redux';
import { createStore, combineReducers } from 'redux';
import { HotTable, HotColumn, useHotEditor } from '@handsontable/react-wrapper';
import { registerAllModules } from 'handsontable/registry';
// register Handsontable's modules
registerAllModules();
type RendererProps = {
TD?: HTMLTableCellElement;
value?: string | number;
row?: number;
col?: number;
cellProperties?: Handsontable.CellProperties;
};
const UnconnectedColorPickerEditor = () => {
const dispatch = useDispatch();
const editorRef = useRef<HTMLDivElement>(null);
const [pickedColor, setPickedColor] = useState('');
const { value, setValue, isOpen, finishEditing, col, row } = useHotEditor({
onOpen: () => {
if (editorRef.current) editorRef.current.style.display = 'block';
(document.querySelector('.react-colorful__interactive') as HTMLDivElement)?.focus();
},
onClose: () => {
if (editorRef.current) editorRef.current.style.display = 'none';
setPickedColor('');
},
onPrepare: (_row, _column, _prop, TD, _originalValue, _cellProperties) => {
const tdPosition = TD.getBoundingClientRect();
if (!editorRef.current) return;
editorRef.current.style.left = `${tdPosition.left + window.pageXOffset}px`;
editorRef.current.style.top = `${tdPosition.top + window.pageYOffset}px`;
},
onFocus: () => {},
});
const onPickedColor = (color: string) => {
setValue(color);
};
const applyColor = () => {
if (col === 1) {
dispatch({
type: 'updateActiveStarColor',
row,
hexColor: value,
});
} else if (col === 2) {
dispatch({
type: 'updateInactiveStarColor',
row,
hexColor: value,
});
}
finishEditing();
};
const stopMousedownPropagation = (e: MouseEvent) => {
e.stopPropagation();
};
const stopKeyboardPropagation = (e: KeyboardEvent) => {
e.stopPropagation();
if (e.key === 'Escape') {
applyColor();
}
};
return (
<div
style={{
display: 'none',
position: 'absolute',
left: 0,
top: 0,
zIndex: 999,
background: '#fff',
padding: '15px',
border: '1px solid #cecece',
}}
ref={editorRef}
onMouseDown={stopMousedownPropagation}
onKeyDown={stopKeyboardPropagation}
>
<HexColorPicker color={pickedColor || value} onChange={onPickedColor} />
<button style={{ width: '100%', height: '33px', marginTop: '10px' }} onClick={applyColor}>
Apply
</button>
</div>
);
};
const ColorPickerEditor = connect(function (state: RootState) {
return {
activeColors: state.appReducer.activeColors,
inactiveColors: state.appReducer.inactiveColors,
};
})(UnconnectedColorPickerEditor);
const ColorPickerRenderer = ({ value }: RendererProps) => {
return (
<>
<div
style={{
background: value,
width: '21px',
height: '21px',
float: 'left',
marginRight: '5px',
}}
/>
<div>{value}</div>
</>
);
};
// a Redux component
const initialReduxStoreState: {
activeColors?: string[];
inactiveColors?: string[];
} = {
activeColors: [],
inactiveColors: [],
};
const appReducer = (
state = initialReduxStoreState,
action: { type?: any; row?: any; hexColor?: any; hotData?: any }
) => {
switch (action.type) {
case 'initRatingColors': {
const { hotData } = action;
const activeColors = hotData.map((data: string[]) => data[1]);
const inactiveColors = hotData.map((data: string[]) => data[2]);
return {
...state,
activeColors,
inactiveColors,
};
}
case 'updateActiveStarColor': {
const rowIndex = action.row;
const newColor = action.hexColor;
const activeColorArray = state.activeColors ? [...state.activeColors] : [];
activeColorArray[rowIndex] = newColor;
return {
...state,
activeColors: activeColorArray,
};
}
case 'updateInactiveStarColor': {
const rowIndex = action.row;
const newColor = action.hexColor;
const inactiveColorArray = state.inactiveColors ? [...state.inactiveColors] : [];
inactiveColorArray[rowIndex] = newColor;
return {
...state,
inactiveColors: inactiveColorArray,
};
}
default:
return state;
}
};
const actionReducers = combineReducers({ appReducer });
const reduxStore = createStore(actionReducers);
type RootState = ReturnType<typeof actionReducers>;
// a custom renderer component
const UnconnectedStarRatingRenderer = ({
row,
col,
value,
activeColors,
inactiveColors,
}: {
row?: number;
col?: number;
value?: number;
activeColors?: string;
inactiveColors?: string;
}) => {
return (
<StarRatingComponent
name={`${row}-${col}`}
value={value}
starCount={5}
starColor={activeColors?.[row || 0]}
emptyStarColor={inactiveColors?.[row || 0]}
editing={true}
/>
);
};
const StarRatingRenderer = connect((state: RootState) => ({
activeColors: state.appReducer.activeColors,
inactiveColors: state.appReducer.inactiveColors,
}))(UnconnectedStarRatingRenderer);
const data = [
[1, '#ff6900', '#fcb900'],
[2, '#fcb900', '#7bdcb5'],
[3, '#7bdcb5', '#8ed1fc'],
[4, '#00d084', '#0693e3'],
[5, '#eb144c', '#abb8c3'],
];
const ExampleComponent = () => {
useEffect(() => {
reduxStore.dispatch({
type: 'initRatingColors',
hotData: data,
});
}, []);
return (
<Provider store={reduxStore}>
<HotTable
data={data}
rowHeaders={true}
rowHeights={30}
colHeaders={['Rating', 'Active star color', 'Inactive star color']}
height="auto"
autoWrapRow={true}
autoWrapCol={true}
licenseKey="non-commercial-and-evaluation"
>
<HotColumn width={100} type="numeric" renderer={StarRatingRenderer} />
<HotColumn width={150} renderer={ColorPickerRenderer} editor={ColorPickerEditor} />
<HotColumn width={150} renderer={ColorPickerRenderer} editor={ColorPickerEditor} />
</HotTable>
</Provider>
);
};
export default ExampleComponent;