React Data GridIntegration with Redux
Maintain the data and configuration options of your grid by using the Redux state container.
Integrate with Redux
TIP
Before using any state management library, make sure you know how Handsontable handles data: see the Binding to data page.
The following example implements the @handsontable/react-wrapper
component with a readOnly
toggle switch and the Redux state manager.
Simple example
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';
import 'handsontable/styles/handsontable.css';
import 'handsontable/styles/ht-theme-main.css';
// 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;
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
).
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';
import 'handsontable/styles/handsontable.css';
import 'handsontable/styles/ht-theme-main.css';
// 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;
There is a newer version of Handsontable available. Switch to the latest version ⟶