Integration with Redux
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
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 modulesregisterAllModules();
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;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 modulesregisterAllModules();
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).
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 modulesregisterAllModules();
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 componentconst 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 componentconst 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;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 modulesregisterAllModules();
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 componentconst 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 componentconst 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;