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 component with a readOnly toggle switch and the Redux state manager.

Simple example

Redux store dump

data:
A1B1C1
A2B2C2
A3B3C3
A4B4C4
A5B5C5
colHeaders:true
rowHeaders:true
readOnly:false
height:auto
licenseKey:non-commercial-and-evaluation
import { useRef } from 'react';
import { createStore } from 'redux';
import { Provider, useSelector, useDispatch } from 'react-redux';
import { HotTable } from '@handsontable/react';
import { registerAllModules } from 'handsontable/registry';
import 'handsontable/dist/handsontable.full.min.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, createRef } from 'react';
import { HexColorPicker } from 'react-colorful';
import StarRatingComponent from 'react-star-rating-component';
import { Provider, connect } from 'react-redux';
import { createStore, combineReducers } from 'redux';
import { HotTable, HotColumn, BaseEditorComponent } from '@handsontable/react';
import { registerAllModules } from 'handsontable/registry';
import 'handsontable/dist/handsontable.full.min.css';

// register Handsontable's modules
registerAllModules();

// a custom editor component
class UnconnectedColorPicker extends BaseEditorComponent {
  constructor(props) {
    super(props);
    this.editorRef = createRef();
    this.state = {
      renderResult: null,
      value: '',
    };
  }
  stopMousedownPropagation(e) {
    e.stopPropagation();
  }
  setValue(value, callback) {
    this.setState((state, props) => {
      return { value };
    }, callback);
  }
  getValue() {
    return this.state.value;
  }
  open() {
    if (!this.editorRef.current) return;
    this.editorRef.current.style.display = 'block';
  }
  close() {
    if (!this.editorRef.current) return;
    this.editorRef.current.style.display = 'none';
    this.setState({
      pickedColor: null,
    });
  }
  prepare(row, col, prop, td, originalValue, cellProperties) {
    super.prepare(row, col, prop, td, originalValue, cellProperties);

    const tdPosition = td.getBoundingClientRect();

    if (!this.editorRef.current) return;
    this.editorRef.current.style.left = `${
      tdPosition.left + window.pageXOffset
    }px`;
    this.editorRef.current.style.top = `${
      tdPosition.top + window.pageYOffset
    }px`;
  }
  onPickedColor(color) {
    this.setValue(color, () => {});
  }
  applyColor() {
    const dispatch = this.props.dispatch;

    if (this.col === 1) {
      dispatch({
        type: 'updateActiveStarColor',
        row: this.row,
        hexColor: this.getValue(),
      });
    } else if (this.col === 2) {
      dispatch({
        type: 'updateInactiveStarColor',
        row: this.row,
        hexColor: this.getValue(),
      });
    }

    this.finishEditing();
  }
  render() {
    let renderResult = null;

    if (this.props.isEditor) {
      renderResult = (
        <div
          style={{
            display: 'none',
            position: 'absolute',
            left: 0,
            top: 0,
            zIndex: 999,
            background: '#fff',
            padding: '15px',
            border: '1px solid #cecece',
          }}
          ref={this.editorRef}
          onMouseDown={this.stopMousedownPropagation}
        >
          <HexColorPicker
            color={this.state.pickedColor || this.state.value}
            onChange={this.onPickedColor.bind(this)}
          />
          <button
            style={{ width: '100%', height: '33px', marginTop: '10px' }}
            onClick={this.applyColor.bind(this)}
          >
            Apply
          </button>
        </div>
      );
    } else if (this.props.isRenderer) {
      renderResult = (
        <>
          <div
            style={{
              background: this.props.value,
              width: '21px',
              height: '21px',
              float: 'left',
              marginRight: '5px',
            }}
          />
          <div>{this.props.value}</div>
        </>
      );
    }

    return <>{renderResult}</>;
  }
}
const ColorPicker = connect(function (state) {
  return {
    activeColors: state.appReducer.activeColors,
    inactiveColors: state.appReducer.inactiveColors,
  };
})(UnconnectedColorPicker);

// 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">
          <StarRatingRenderer hot-renderer />
        </HotColumn>
        <HotColumn width={150}>
          <ColorPicker hot-renderer hot-editor />
        </HotColumn>
        <HotColumn width={150}>
          <ColorPicker hot-renderer hot-editor />
        </HotColumn>
      </HotTable>
    </Provider>
  );
};

export default ExampleComponent;

There is a newer version of Handsontable available. Switch to the latest version ⟶