DevExtreme React Grid - Batch Saving

Oliver's Blog
07 July 2017

This post is part of a series about the DevExtreme React Grid. You can find the introduction and overview to the post series by following this link.

In this post, I want to take advantage of the fact that the Redux store gives me external access to the Grid state. As I outlined in the previous post, individual React component can certainly have internal state, even if they are used in an application where overall state management is done with Redux. But sometimes state can be used externally for good reason, and in this example I will implement the functionality of batch saving (and batch discarding of changes), which is not currently implemented by the grid itself.

Note that the data structures used by the Grid (or specifically, by the EditingState) already take into account that multiple editing operations can happen in parallel. In fact, you can edit multiple rows of data out of the box. But the standard behavior supplies only a per-row Save link, and if the EditingState event onCommitChanges is triggered, there is only ever one change to be committed. Batch Editing should be fully implemented in the Grid at a later point.

Creating a toolbar

At the beginning of my sample code (as usual, you can find the whole sample at the bottom of this post), I import two UI elements from the react-bootstrap library:

const { ButtonToolbar, Button } = ReactBootstrap;

With the help of these two components, I define a toolbar. You see that I’m retrieving the event handlers from props, so you might guess that this component will be integrated as a connected component, just like I did with the Grid in the previous post.

class ReduxToolbar extends React.PureComponent {
  render() {
    const {
      gridHasEditingChanges,
      onSaveButtonClick,
      onDiscardButtonClick
    } = this.props;
    return (
      <ButtonToolbar>
        <Button
          bsStyle="success"
          disabled={!gridHasEditingChanges}
          onClick={onSaveButtonClick}
        >
          Save Changes
        </Button>
        <Button
          bsStyle="danger"
          disabled={!gridHasEditingChanges}
          onClick={onDiscardButtonClick}
        >
          Discard Changes
        </Button>
      </ButtonToolbar>
    );
  }
}

In addition to the event handlers, I also receive a flag called gridHasEditingChanges, which is used to activate and deactivate the buttons. This flag represents a piece of grid-specific state information, which is supplied to the toolbar component.

Changes to the Grid configuration

I am not using an onCommitChanges event handler anymore on the EditingState. I added a commandTemplate to the TableEditColumn that removes the Save link that is normally shown when the user edits a row. As a result, saving of individual rows is no longer possible.

<TableEditColumn allowAdding allowEditing allowDeleting 
  commandTemplate={({ id }) => (id === 'commit' ? null : undefined)} />

The commandTemplate function receives an id as a parameter. I’m testing this to see whether I’m looking at the Save command (which has the id commit), and then I return either null or undefined. This may seem confusing and we are considering other options, but the distinction is that null is handled by React in its standard way of not rendering anything at all, while undefined is interpreted by our libraries and results in the standard element being rendered.

Changes to the Redux elements

Compared to the sample from the previous post, I have removed the GRID_SAVE action. Instead, I have introduced the following three new actions:

const BATCH_SAVE = "BATCH_SAVE";
const BATCH_DISCARD = "BATCH_DISCARD";
const GRID_EDITING_STATE_CHANGE = "GRID_EDITING_STATE_CHANGE";

const batchSave = () => ({
  type: BATCH_SAVE
});

const batchDiscard = () => ({
  type: BATCH_DISCARD
});

const gridEditingStateChange = (stateFieldName, stateFieldValue) => ({
  type: GRID_EDITING_STATE_CHANGE,
  stateFieldName,
  stateFieldValue
});

The BATCH_SAVE and BATCH_DISCARD actions will be dispatched by the event handlers of the toolbar buttons. GRID_EDITING_STATE_CHANGE is a variation of the standard GRID_STATE_CHANGE and it will be handled specially to track whether or not the editing state currently contains any change information.

To handle GRID_EDITING_STATE_CHANGE, I have added this block to the grid reducer:

case GRID_EDITING_STATE_CHANGE:
  const { editingRows, changedRows, addedRows, deletedRows } = state;
  const es = {
    editingRows,
    changedRows,
    addedRows,
    deletedRows
  };
  es[action.stateFieldName] = action.stateFieldValue;

  const hasEditingChanges =
    (es.editingRows && es.editingRows.length > 0) ||
    (es.addedRows && es.addedRows.length > 0) ||
    (es.deletedRows && es.deletedRows.length > 0) ||
    (es.changedRows && Object.keys(es.changedRows).length > 0);

  return {
    ...state,
    hasEditingChanges,
    [action.stateFieldName]: action.stateFieldValue
  };

The grid state value hasEditingChanges, which I mentioned before as it was being used by the new toolbar, is calculated here depending on the state and the action currently being handled.

The handling of BATCH_DISCARD is quite simple:

function discardChangeDetails(state) {
  return {
    ...state,
    editingRows: [],
    addedRows: [],
    changedRows: {},
    deletedRows: [],
    hasEditingChanges: false
  };
}

// ... in reducer:

  case BATCH_DISCARD:
    return discardChangeDetails(state);

The helper discardChangeDetails returns a new state with all the editing related fields reset to their default values. I have created this helper function because it is also used by the BATCH_SAVE action handling. The saving logic itself has not changed from the previous version:

case BATCH_SAVE:
  return _.flow(
    Array.from(
      commitChanges(state.addedRows, state.changedRows, state.deletedRows)
    )
  )(state);

In the previous post, I mentioned how commitChanges creates a sequence of function calls to reflect the changes recorded in state. For this version, I simply extended that sequence by adding a call to discardChangeDetails:

function* commitChanges(added, changed, deleted) {
  yield* deleteFunctions(deleted);
  yield* changeFunctions(changed);
  yield* addFunctions(added);
  yield discardChangeDetails;
}

In other words, after changes have been committed, the resulting state will be passed to discardChangeDetails, where the change details are removed from state before that state is returned.

Note that there is currently an issue with the handling of delete state, which means that deletion doesn’t work correctly in the current version of my sample. The reason for this is that there is some special handling for the Delete link: you don’t have to click Save after clicking Delete, because this is triggered automatically. Unfortunately I found that this built-in behavior collides with the concept of batch-saving externally. Our devs are looking into this and I will post about it again in the future.

Integrating the toolbar

To make the new toolbar into a connected component, I need the same elements that I’m using for the grid: a mapStateToProps function, a mapDispatchToProps function and a call to connect.

const mapToolbarStateToProps = state => ({
  gridHasEditingChanges: state.hasEditingChanges
});

const mapToolbarDispatchToProps = dispatch => ({
  onSaveButtonClick: () => dispatch(batchSave()),
  onDiscardButtonClick: () => dispatch(batchDiscard())
});

const ConnectedToolbar = connect(
  mapToolbarStateToProps,
  mapToolbarDispatchToProps
)(ReduxToolbar);

These three elements are pretty straight-forward. The only new thing is the way mapToolbarStateToProps accesses a piece of state information that comes from the grid.

Note that the general recommendation for connected components is to supply them with the precise set of props they require. This statement is meant for each individual component! Theoretically you could have a wrapper that incorporates both the toolbar and the grid, and then push all the relevant state for both components into the common parent. This saves you time creating the mapStateToProps and mapDispatchToProps functions for one of the components. However, it also means that if any of the overall state changes, the parent component with both components inside it would now be re-rendered. In my sample, when gridHasEditingChanges changes, only the toolbar needs to re-render, not the grid. By converting individual components into connected components and supplying them with the distinct state details they require, you optimize your application performance.

The final part that changes is the call to ReactDOM.render, since I’m adding the ConnectedToolbar into the output:

ReactDOM.render(
  <div>
    <Provider store={store}>
      <div>
        <ConnectedToolbar />
        <ConnectedGrid />
      </div>
    </Provider>
  </div>,
  document.getElementById("app")
);

And that’s it. Here is the complete sample, and just like the previous post I recommend using the CodePen debug view and the Redux DevTools Extension to see exactly how state management and action handling work.

Click here to return to the blog series overview

Free DevExpress Products - Get Your Copy Today

The following free DevExpress product offers remain available. Should you have any questions about the free offers below, please submit a ticket via the DevExpress Support Center at your convenience. We'll be happy to follow-up.
No Comments

Please login or register to post comments.