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.
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.
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':
{
let newItem = Object.freeze(copyObject({}, action.item,
{ id: state.idCounter }));
return copyObject({}, state, {
items: state.items.concat([newItem]),
idCounter: state.idCounter + 1
});
}
case 'REMOVE_ITEM':
{
let items = state.items,
index = action.index;
return copyObject({}, state, {
items: items.slice(0, index).concat(items.slice(index + 1))
});
}
case 'CHANGE_ITEM':
{
let items = state.items,
index = action.index,
oldItem = items[index],
clonedItem = Object.freeze(copyObject({}, oldItem, action.item));
return copyObject({}, state, {
items: items.slice(0, index).
concat([clonedItem]).
concat(items.slice(index + 1))
});
}
case 'CHANGE_COUNT':
{
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';
import { appReducer } from './reducers';
import { GridViewContainer } from './GridViewContainer';
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!