Click here to Skip to main content
14,643,704 members
Articles » Third Party Products » Product Showcase » General
Article
Posted 14 Sep 2020

Stats

3.2K views
1 bookmarked

How to Create Editable DataGrids in a React Redux Application

14 Sep 2020CPOL
In this article we look at the FlexGrid extension component called ImmutabilityProvider, which addresses the problem where if you want Redux, you have to give up editable datagrids.
Here we take a deeper look at ImmutabilityProvider. Then we learn more details about how to create a simple application that uses a datagrid to show and edit an array from the Redux Store. We also look at View Components - Presentational and Container, and then we bring all applications pieces together and run the root App component.

This article is in the Product Showcase section for our sponsors at CodeProject. These articles are intended to provide you with information on products and services that we consider useful and of value to developers.

The following article is accompanied by this sample.

Developers Love Redux

Redux is a popular application architecture nowadays, and especially in the React community. It encourages developers to use a one-way data flow, and to apply changes to data in a single place, in the global Redux Store, using Redux reducers. It's promised that this architecture makes the application more reliable and easier to maintain. This is why many developer teams choose it as a base for their application architecture.

One of the key requirements of this paradigm is to keep data immutable. All changes to data must be made by cloning existing data in the Redux reducers. If you need to add an item into an array, you should create a clone of this array with the new item added into it. If you need to change an item property, you should create a clone of this item, containing the new value of this property. You should never mutate existing arrays and objects!

Applications Love Editable DataGrids

At the same time, developers want to use datagrid components as a part of their application UI. It's not a secret that a datagrid component is a key part of any web UI component library. There is a simple explanation for this. Datagrids allow your application's users not only to see a complex tabular data in a conveniently compact form, and efficiently perform data transformations (like sorting, grouping, and filtering). It allows users to edit data exactly the way you would edit data in Microsoft Excel.

Excel allows the user to edit item values, right in the datagrid cells. You can add or delete items, copy-paste data from Excel or another datagrid, clear multiple selected cells, and so on.

This makes the datagrid the #1 guest (or sometimes even a host) in many modern web applications.

Editable DataGrids Don't Love Redux

On the one hand, you want to use an advanced application architecture based on Redux. On the other hand, you want to have editable datagrids as a part of your application UI. You may ask "what's the problem," just do it, right? And here we come to the most interesting point.

The correct answer is "No." (more precisely, the answer used to be "No.")

The problem lies in that datagrid components are designed to mutate the data they are bound to directly. And this is understandable; for any "usual" application, this is exactly what it needs. But it doesn't work with Redux, with its requirement for the data immutability.

So, you face a dilemma; if you want Redux, you have to give up editable datagrids. And this way, to impoverish the user experience provided by your application.

Sounds awful, advanced application architecture doesn't allow you to use advanced UI components. And there is no obvious way to solve this problem. Special libraries like Immer and Immutable help you to solve problems you may face in Redux reducers, but not this specific one.

What to do? Give up the cool UI? Not with FlexGrid!

FlexGrid Loves Redux

We recognized this problem here in the Wijmo team (it should be noted that not without the help of our customers), and introduced a very easy to use FlexGrid extension component called ImmutabilityProvider. Being applied to the FlexGrid component, it changes its behavior in the following ways:

  • Prevents the datagrid from mutating the underlying data while keeping all its data editing capabilities. That is, a user can edit data via FlexGrid in all possible ways, but the underlying data array from the Redux Store remains immutable.
  • When a user edits data in FlexGrid, ImmutabilityProvider fires a special event with the information about the change, which can be used to dispatch data change actions to the Redux Store.

Now let's see how it works.

ImmutabilityProvider

The simplest JSX that adds a data bound FlexGrid control in the render method of your component may look like here:

<FlexGrid itemsSource={this.props.items}>
</FlexGrid>

This datagrid will mutate the data array bound to its itemsSource property, when a user edits data via the datagrid. To change this behavior and to force FlexGrid to stop mutating the underlying data, we nest the ImmutabilityProvider React component into the FlexGrid component, as shown here:

<FlexGrid>
    <ImmutabilityProvider 
        itemsSource={this.props.items}
        dataChanged={this.onGridDataChanged} />
</FlexGrid>

Note that the itemsSource property is specified on the ImmutabilityProvider component now, but not on FlexGrid. And we also defined a handler for the dataChanged event, which notifies us about three possible types of data changes happening in the datagrid, as a result of user's edits:

  • existing item's property values was changed
  • new item were added
  • item was deleted

When this event is triggered, even though visually everything looks like the data was changed, the underlying items array remains unchanged (both the array itself and its items' properties).

We use this event to dispatch corresponding data change actions to the Redux Store to apply the changes made by a user to the global application state. The event handler may look like here:

onGridDataChanged(s: ImmutabilityProvider, e: DataChangeEventArgs) {
    switch (e.action) {
        case DataChangeAction.Add:
            this.props.addItemAction(e.newItem);
            break;
        case DataChangeAction.Remove:
            this.props.removeItemAction(e.newItem, e.itemIndex);
            break;
        case DataChangeAction.Change:
            this.props.changeItemAction(e.newItem, e.itemIndex);
            break;
        default:
            throw 'Unknown data action'
    }
}

Depending on the type of data change occurred in the FlexGrid (Add, Remove or Change), the event handler will dispatch a corresponding action to the Redux Store's reducer. The latter will update the global state with a clone of the array containing the dispatched changes. Because this array is directly bound to the ImmutabilityProvider.itemsSource property, the latter will detect the change and cause FlexGrid to refresh its content to reflect the changes happened in the Store.

Despite the seemingly complex data flow, the performance is fine even on pretty big data. The changes made by a user are applied almost instantly.

With this approach, the use of the datagrid as a data editing control in the Redux application becomes almost as simple as the use of one-value input controls (like native input element, or specialized InputNumber, InputDate and so on), where you bind a control value property to the global state property, and dispatch an action with a new value in the control's "value changed" event.

More React-Redux Details

Let's now learn more details about how to create a simple application that uses a datagrid to show and edit an array from the Redux Store. For the demonstration purposes, we'll use this sample. It's intentionally made very simple to let you easier understand the essence of the solution to the discussing problem. In the future, we plan to add more samples demonstrating various aspects of the FlexGrid and other Wijmo controls use in a Redux application. If you have any specific issues in mind, which you want us to highlight, please don't hesitate to share your suggestions with us!

The sample tries to follow the standard React-Redux application structure, but with a flattened folder structure to better fit it into the Wijmo online Demo site. Also, because of the Demo site requirements to the setup of the sample, it uses SystemJS run-time loader to load modules, instead of Webpack or a similar bundler.

Image 1

The application has a single view containing two FlexGrid controls. The top one is the editable datagrid, controlled by the ImmutabilityProvider component, which is bound to the array from the Redux Store. This is the datagrid to check the functionality (what we are discussing in this article). You can edit cell values by typing them from the keyboard, add new items using the "new row" row at the end of the grid rows list, or delete items by selecting them and pressing the Delete button.

You can also paste data from the Clipboard, or clear multiple cell values in the selected cells range.

It's important to mention that all items in the data array displayed in the datagrid are frozen using the Object.freeze() function, to ensure that the datagrid doesn't mutate the data when you are performing edits.

In addition to editing, you can also transform the data as you need - sort by clicking on a column header, group by dragging column headers to the group panel above the datagrid, and filter it by clicking on the filter icons in the column headers.

The second datagrid is read-only. It doesn't use ImmutabilityProvider, and is bound directly to the Store's array using its itemsSource property. This datagrid can help you to check how changes made via the top datagrid are applied to the Redux Store.

There is also a menu above the top datagrid, which allows you to change the size of a data array. Small arrays are handy to check how your changes are applied to the Store. Bigger arrays can be used to evaluate the performance of the editing process. You can choose several items similar to what you expect in your real application, and assess how it works.

State

The initial application global state is defined in the reducers.jsx file as follows:

const itemCount = 5000;
const initialState = {
    itemCount,
    items: getData(itemCount),
    idCounter: itemCount
}

It contains the items array with a randomly generated data, and a couple of auxiliary properties - itemCount, which defines the number of items in the array, and idCounter, which stores a unique id value to assign to the id property of newly added items to the items array. The items array is the one represented by the sample's datagrids.
Every item in the array is frozen using the Object.freeze() function, to ensure that the data immutability requirement imposed by Redux is really satisfied.

Actions

Redux action creator functions are defined in the actions.jsx file:

export const addItemAction = (item) => ({
    type: 'ADD_ITEM',
    item
});

export const removeItemAction = (item, index) => ({
    type: 'REMOVE_ITEM',
    item,
    index
});

export const changeItemAction = (item, index) => ({
    type: 'CHANGE_ITEM',
    item,
    index
});

export const changeCountAction = (count) => ({
    type: 'CHANGE_COUNT',
    count
});

There are three actions intended for the data change operations performed on the items array (ADD_ITEM, REMOVE_ITEM, and CHANGE_ITEM), and one additional CHANGE_COUNT action which causes the Store to create a brand new items array with a different number of items.

Every action is represented by the action creator function. These functions are called in the ImmutabilityProvider.dataChanged event handler (in the GridView presentation component), to notify the Store about data changes made in the datagrid.

For the item change actions, the index property contains an index of the affected item in the items array. And the item property stores a reference to an item object. More about this below, in the Reducer topic.

Reducer

The application defines a single reducer performing all the updates to the application global state, based on the actions described above. It's implemented in the reducers.jsx file:

export const appReducer = (state = initialState, action) => {
    switch (action.type) {
        case 'ADD_ITEM':
            {
                // make a clone of the new item which will be added to the
                // items array, and assigns its 'id' property with a unique value.
                let newItem = Object.freeze(copyObject({}, action.item, 
                        { id: state.idCounter }));
                return copyObject({}, state, {
                    // items array clone with a new item added
                    items: state.items.concat([newItem]),
                    // increment 'id' counter
                    idCounter: state.idCounter + 1
                });
            }
        case 'REMOVE_ITEM':
            {
                let items = state.items,
                    index = action.index;
                return copyObject({}, state, {
                    // items array clone with the item removed
                    items: items.slice(0, index).concat(items.slice(index + 1))
                });
            }
        case 'CHANGE_ITEM':
            {
                let items = state.items,
                    index = action.index,
                    oldItem = items[index],
                    // create a cloned item with the property changes applied
                    clonedItem = Object.freeze(copyObject({}, oldItem, action.item));
                return copyObject({}, state, {
                    // items array clone with the updated item
                    items: items.slice(0, index).
                        concat([clonedItem]).
                        concat(items.slice(index + 1))
                });
            }
        case 'CHANGE_COUNT':
            {
                // create a brand new state with a new data
                let ret = copyObject({}, state, {
                    itemCount: action.count,
                    items: getData(action.count),
                    idCounter: action.count
                });
                return ret;
            }
        default:
            return state;
    }
}

As Redux requires, we never mutate existing items array, as well as properties of its items. If we add or delete an item, we create a clone of the array where this item is added or removed. If the action prescribes us to update properties of an existing item, we create a new array where the updated item is replaced with the clone of the changed item.

This cloned item has new values for the properties that were changed. We use the copyObject function from the @grapecity/wijmo.grid.immutable module to clone the objects. It effectively uses the Object.assign function if it's implemented by the browser, and falls back to the custom implementation if not (for example, in IE).

To process the REMOVE_ITEM and CHANGE_ITEM actions, we need to know an existing item in the items array affected by this change, and/or its index. In this sample, we use the simplest and fastest way to do it. The index of the item is passed in the index property of the action's data (the ImmutabilityProvider.dataChanged event brings this information to you!).

If this approach doesn't work for you due to some reason, you can alternatively pass the original item which is about to change, in the action data. And to find its index using the items.indexOf() method. Or to search it by an item id, if it makes more sense to you.

For the CHANGE_ITEM action, you need not only to know the existing item which is about to change but also what are the new property values of the item. The ImmutabilityProvider.dataChanged event's data brings this information as well, by providing you with a cloned object containing the new item property values. This cloned object is passed in the action's item property, and is used by the reducer to create a new cloned item with new property values, to use it in the cloned items array instead of the old one.

Note that for any cloned item added to a cloned items array, we call Object.freeze to protect the item from accidental unintentional mutation.

View Components - Presentational and Container

The UI of the sample is implemented in the single GridView presentational component, in the GridView.jsx file. And as is customary in the React bindings for Redux, we use it together with the container component - GridViewContainer implemented in the GridViewContainer.jsx file. The latter is just a wrapper over the former, intended to provide the presentational GridView with the necessary data from the Redux Store.

The data is the items array represented in the datagrid, and the action creator functions (addItemAction, removeItemAction, and so on). It will be available to GridView as props, via the this.props object.

Here's how GridViewContainer is implemented:

import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { GridView } from './GridView';
import { addItemAction, removeItemAction, changeItemAction, changeCountAction } from './actions';


const mapStateToProps = state => ({
    items: state.items,
    itemCount: state.itemCount
})
const mapDispatchToProps = dispatch => {
    return bindActionCreators(
        { 
            addItemAction, removeItemAction, changeItemAction, changeCountAction 
        }, 
        dispatch
    );
};

export const GridViewContainer = connect(
    mapStateToProps,
    mapDispatchToProps
  )(GridView);

The GridView presentational component adds a FlexGrid component with an associated ImmutabilityProvider using this code in the component's render method:

import * as wjFlexGrid from '@grapecity/wijmo.react.grid';
import * as wjGridFilter from '@grapecity/wijmo.react.grid.filter';
import { DataChangeEventArgs, DataChangeAction } from '@grapecity/wijmo.grid.immutable';
import { ImmutabilityProvider } from '@grapecity/wijmo.react.grid.immutable';
....


<wjFlexGrid.FlexGrid
        allowAddNew 
        allowDelete
        initialized={this.onGridInitialized}>
    <ImmutabilityProvider 
        itemsSource={this.props.items}
        dataChanged={this.onGridDataChanged} />
    <wjGridFilter.FlexGridFilter/>
    <wjFlexGrid.FlexGridColumn binding="id" header="ID" width={80} isReadOnly={true}></wjFlexGrid.FlexGridColumn>
    <wjFlexGrid.FlexGridColumn binding="start" header="Date" format="d"></wjFlexGrid.FlexGridColumn>
    <wjFlexGrid.FlexGridColumn binding="end" header="Time" format="t"></wjFlexGrid.FlexGridColumn>
    <wjFlexGrid.FlexGridColumn binding="country" header="Country"></wjFlexGrid.FlexGridColumn>
    <wjFlexGrid.FlexGridColumn binding="product" header="Product"></wjFlexGrid.FlexGridColumn>
    <wjFlexGrid.FlexGridColumn binding="sales" header="Sales" format="n2"></wjFlexGrid.FlexGridColumn>
    <wjFlexGrid.FlexGridColumn binding="downloads" header="Downloads" format="n0"></wjFlexGrid.FlexGridColumn>
    <wjFlexGrid.FlexGridColumn binding="active" header="Active" width={80}></wjFlexGrid.FlexGridColumn>
</wjFlexGrid.FlexGrid>

As you can see, ImmutabilityProvider has its itemsSource property bound to the this.props.items property, which contains the items array from the global application state. Every time the Store reducer will produce a new clone of the array to apply user changes, this.props.items will be automatically updated with the new array instance, and ImmutabilityProvider will cause FlexGrid to update its content to reflect the changes.

The dataChanged event of the ImmutabilityProvider is called each time a user makes and saves changes to data in the datagrid. It's bound to the onGridDataChanged handler function, which is implemented as follows:

onGridDataChanged(s: ImmutabilityProvider, e: DataChangeEventArgs) {
    switch (e.action) {
        case DataChangeAction.Add:
            this.props.addItemAction(e.newItem);
            break;
        case DataChangeAction.Remove:
            this.props.removeItemAction(e.oldItem, e.itemIndex);
            break;
        case DataChangeAction.Change:
            this.props.changeItemAction(e.newItem, e.itemIndex);
            break;
        default:
            throw 'Unknown data action'
    }
}

The handler just calls an appropriate action creator function, which is also available to the GridView component via the this.props object thanks to the GridViewContainer container component.
Action data is retrieved from the event argument of the DataChangeEventArgs type. It brings information about the change action performed (the action property, can take Add, Remove or Change values), index of the affected item in the source array (index), and a reference to the affected item (oldItem or newItem, depending on the data action).

A special case here is the Change action, where both the oldItem and newItem properties are used. oldItem contains an original (unchanged) item whose property values must be changed, and newItem contains a clone of the original item with the new property values.

So, instead of mutating the source array directly, FlexGrid with the attached ImmutabilityProvider fires the dataChanged event, which calls an appropriate action creator function, using the data provided by the event. This call dispatches the action to the Redux Store, after that it gets to the Store's reducer.

The reducer creates a clone of the array with the changes to data applied, and this new copy of the array becomes available in the this.props.items property bound to the ImmutabilityProvider.itemsSource property. ImmutabilityProvider detects this new array instance and causes FlexGrid to refresh its content.

The view includes a Menu component, which allows a user to change the size of an array displayed in the datagrid. Changing its value causes Redux Store to create a new items array of the specified length. The menu is added to the view using this code in the component's render method:

import * as wjInput from '@grapecity/wijmo.react.input';
....


<wjInput.Menu header='Items number'
    value={this.props.itemCount}
    itemClicked={this.onCountChanged}>
    <wjInput.MenuItem value={5}>5</wjInput.MenuItem>
    <wjInput.MenuItem value={50}>50</wjInput.MenuItem>
    <wjInput.MenuItem value={100}>100</wjInput.MenuItem>
    <wjInput.MenuItem value={500}>500</wjInput.MenuItem>
    <wjInput.MenuItem value={5000}>5,000</wjInput.MenuItem>
    <wjInput.MenuItem value={10000}>10,000</wjInput.MenuItem>
    <wjInput.MenuItem value={50000}>50,000</wjInput.MenuItem>
    <wjInput.MenuItem value={100000}>100,000</wjInput.MenuItem>
</wjInput.Menu>

The value property of the menu is bound to the itemCount property of the global Redux state, which contains the current items array length. When a user selects another value in the menu drop-down list, the itemClicked event is triggered and calls the onCountChanged event handler function, which has this simple implementation:

onCountChanged(s: wjcInput.Menu) {
    this.props.changeCountAction(s.selectedValue);
}

The handler just calls the changeCountAction action creator function, passing the new array length as the action data. This forces the Store reducer to create a new items array of the specified length.
And the other UI element of the view is a read-only datagrid, which just displays the content of the items array.

The datagrid has the associated "Show data" checkbox element, which allows a user to temporarily disconnect the datagrid from the data array. Here's the JSX from the component's render method that adds these components:

<input type="checkbox" 
    checked={this.state.showStoreData}
    onChange={ (e) => { 
        this.setState({ showStoreData: e.target.checked}); 
} } /> 
<b>Show data</b>
<wjFlexGrid.FlexGrid 
    itemsSource={this.state.showStoreData ? this.props.items : null} 
    isReadOnly/> 
</div>

The "Show data" checkbox is a controlled component, which stores its value in the showStoreData property of the component's state. We use a local component state here to store this value because this is a view of the specific property, which doesn't make sense for the rest of the application. But if you prefer to store everything in the global Redux state, no problems, it can be easily moved there.
Note that FlexGrid.itemsSource property is conditionally bound to the Store's items array, or to a null value, depending on the showStoreData property value.

Bringing Everything Together

The entry point of the application is the app.jsx file, where we bring all applications pieces together and run the root App component:

import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
//Application
import { appReducer } from './reducers';
import { GridViewContainer } from './GridViewContainer';

// Create global Redux Store
const store = createStore(appReducer);

class App extends React.Component<any, any> {
    render() {
        return <Provider store={store}>
            <GridViewContainer />
          </Provider>;
    }
}

ReactDOM.render(<App />, document.getElementById('app'));

As you see, we create an application store, passing it to our reducer. And we render the GridViewContainer container component, which will, in turn, render the GridView presentational component, passing it the global Store data as props. We wrap the application component tree with the react-redux Provider component to make the store easily accessible from any application component.

Conclusion

The FlexGrid datagrid together with the associated ImmutabilityProvider component allows you to take the best from two worlds - Redux based application state management and editable datagrids. Using it, you can use editable datagrids in your application UI, without compromising the Redux requirement for the data immutability. The performance of this solution is fine even on pretty big data.

The use of the datagrid as a data editing control in the Redux application becomes almost as simple as the use of input controls, where you bind a control value to the global state value, and dispatch an action with a new value in the control's "value changed" event.

You can find the on-line version of the sample used in this article, and download its source code for the offline investigation here.

The documentation for the ImmutabilityProvider can be found here.

Check out our sample.

Read the full Wijmo 2020 v1 release.

If you have something interesting that you want to bring to our attention, we would love to hear it!

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Alex Ivanenko
United States United States
Alex Ivanenko brings more than 30 years of experience in the software industry to the Wijmo Program Manager role. He graduated from Lomonosov Moscow State University with a Master degree in Applied Mathematics and Cybernetics. Alex enjoys working with people of different cultures and backgrounds, as well as technical experience at GrapeCity. When he isn't managing a new Wijmo project, you can find him traveling, fishing, or picking mushrooms.

Comments and Discussions

 
QuestionI like react/redux Pin
Sacha Barber16-Sep-20 10:29
MemberSacha Barber16-Sep-20 10:29 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.