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.
At the same time, this post is part of a series describing a demo project that employs various real-world patterns and tools to provide access to data in a MongoDB database for the DevExtreme grid widgets. You can find the introduction and overview to the post series by following this link.
The sample described in this blog post loads data from the data service implemented for my DevExtreme - Real World Patterns post series. The data access pattern has been described previously in this post.
I have created a new branch react-frontend for the big sample project and added a React based web application to it. The functionality of this application will be largely familiar to you by now if you’ve been following my DevExtreme - React Grid blog series. It utilizes most of the standard functionality of the React Grid, it uses Redux and the various details I pointed out in previous posts are implemented in similar ways.
New elements and differences compared to the CodePen samples
In comparison with the samples I created in CodePen to illustrate previous posts, there are several items I want to mention quickly since they differ in the sample application for this post. This is probably not a complete list, but it should cover the major differences.
1 - Various components and other application building blocks live in their own code files. This is of course an obvious best practice, but in the CodePen samples everything was in one “file”. The file App.js is now the place where the initialization logic lives.
2 - The toolbar has been extended to accommodate a reload button, a button to create test data, selection buttons for the UI library to use, and a checkbox to toggle the use of custom editors. As a result, there is now state information associated with the toolbar and it has its own reducer. Using the Redux helper combineReducers, the two reducers for the grid and the toolbar are merged for each to handle a piece of the total state.
3 - I’m also using the package redux-first-router to provide navigation functionality in the app (i.e. for now you see different URLs when you use different UI versions of the grid). The state handled by the routing-specific reducer is also included in the previously described call to combineReducers
.
4 - I’m using the package redux-saga to handle situations where Redux actions result in other actions being triggered, and/or side effects need to be executed. Two sagas are “run” on application startup, one for the toolbar and one for the grid.
5 - Finally, since I have implemented a feature to allow the user to switch between Bootstrap and Material UI, I have written the Grid
and Toolbar
code in such a way that both UI variations are created by the same components. I made this decision so that I could point out how the JSX rendered for a component doesn’t change at all if you select a different UI platform (this is true at least for the Grid, while the toolbar uses UI-specific components anyway). However, as we will add supported UI librariesfor React Grid over time, I will probably reconsider this approach for ease of maintenance.
Working with remote data
The structure of the React Grid is such that data is always supplied to it via its props. You have seen in previous samples of this blog series how that data can be kept in component state and how it can be loaded from a Redux store. I am still using Redux in this new sample, but now I trigger actions to initiate the loading and editing of data and modify the state accordingly.
Towards the end of this post, you will find a summary of the big steps you have to go through in order to implement remote data loading. I also recommend checking out this demo, which implements a read-only grid working with remote data, in a much more restricted but also minimal way (compared to my sample).
Data loading
The loading process is initially triggered by the ReduxGrid
component in its componentDidMount lifecycle method:
componentDidMount() { this.props.dispatch(gridLoad()); }
Since the loading of data requires triggering side-effects, I’m handling this action in the grid saga. Here is the handling function:
function* gridLoadHandler(action) { yield put(gridStateChange('loading', true)); const loadOptions = yield select(getLoadOptions); const data = yield call(loadData, loadOptions, action.force); if (data) yield put(gridDataLoaded(data)); else yield put(gridStateChange('loading', false)); }
The process has five steps. First, the grid’s loading
state is set to true using the gridStateChange
action (put simply triggers another action). This results in a loading indicator being shown, i.e. a transparent panel on top of the grid component with a spinning indicator.
Second, I use the redux-saga select call to access information from the grid state in the Redux store. The getLoadOptions
helper extracts the fields relevant for data loading.
In the third step, call is used to run the function loadData
, which I will explain in a bit more detail below. This is the side effect I was mentioning before, since the data loading process is asynchronous.
Step four triggers another action, gridDataLoaded
, if and when data has been received. This action is handled by the normal grid reducer and its implementation simply includes the newly loaded data and the associated totalCount
into grid state.
case GRID_DATA_LOADED: return { ...state, rows: action.data.rows, totalCount: action.data.totalCount, loading: false };
Finally the loading
flag is reset in step five, which removes the loading indicator.
The functionality of loadData
The function loadData
in grid-saga.js is just a small wrapper around fetchData
in data-access.js. The general loading process looks like this:
1 - Create a query string on the basis of the grid state. As part of the process, the grid state is translated into load options compatible with the DevExtreme data service.
2 - Check that the new query string is different from the previously used one. This is an optimization present in the current codebase, to avoid duplicate loading in some circumstances. An option is supported to force loading (for the reload feature).
3 - Distinguish between group queries and others (simple queries). Initiate loading via the Fetch API and handle the results. This happens in the functions groupQuery
and simpleQuery
.
4 - For simple queries, the result handling consists only of a simple call that changes the top-level result format (convertSimpleQueryResult
). For group queries (createGroupQueryData
), the process is complicated because the structures of the data returned from the service and that required by the React Grid are very different. For efficiency, only nested data of groups that are actually expanded is retrieved in separate queries.
At this point, I don’t want to elaborate on the details of group data creation. You can find the current implementation here, if you’re interested. A few details are likely going to change about the grouping data structures in the future, and I will try to write a summary post about the topic when things are finalized.
Saving data
Changes to the editing state are captured in this sample just like in the previous post, including the hasEditingChanges
flag. The Save button on the toolbar dispatches a BATCH_SAVE
action which is handled in grid-saga.js:
function* batchSaveHandler(action) { const commitParams = yield select(getCommitParams); yield call(commitChanges, commitParams); yield put(gridResetEditingState()); yield delay(100); yield put(gridLoad(true)); }
The save handler has several steps. commitParams
holds the values extracted from grid state that are relevant for saving. The function commitChanges
is called with these parameters, and together with sendChanges
towards the end of data-access.js this sends all the change details to the server using POST and PUT requests. There is room for optimization here for cases where many rows need to be changed or added at the same time, but since the editing scenario is an interactive one based on user input, the realistic number of concurrent edits shouldn’t be too large.
Moving on, the action handler dispatches the GRID_RESET_EDITING_STATE
action, which removes the change details from grid state.
At this point, the handler delays for a moment. This is a workaround solution for the problem that the grid is simply too fast – without the delay, the changed data would not actually be loaded back from the server in the next step. In real-world applications, where you might have certain backend services running on different machines, CQRS and Event Sourcing in action etc, this kind of issue is very common. A hard-coded delay is obviously not very elegant, especially since it might be too large in some cases and too small in others. I recommend reading this blog post for an example that implements update notifications – with this approach, the grid wouldn’t have to reload actively at this point, since it would be notified by the server if and when updates to the data are available.
Finally, the handler dispatches a GRID_LOAD
action, passing true
for the force
flag to make sure a reload takes place. This reload is quite unavoidable in a remote data scenario, since the changes that were committed might have influenced sort order, grouping etc in ways that the client can’t easily predict.
Summary of steps for remote data support
1 - Find the correct places in your architecture from where loading and refreshing of data can be triggered. In my sample I’m reflecting this “data life cycle” in Redux actions, but other approaches are possible. For instance, you can use the componentDidMount
and componentDidUpdate
lifecycle methods of your React component, like our Remote Data demo shows. If you go that way, be aware that you need to control your component updates precisely and prevent data reloading for situations where it’s not required, since componentDidUpdate
is called quite frequently while the user interacts with the Grid.
2 - Execute the logic that loads your data. Once data is available, put it in a place where a Grid update is triggered, e.g. state or props, or a Redux store or similar that interfaces with state or props.
3 - For editing support, you can implement an onCommitChanges
handler on the EditingState
, like I did in this post. Be aware though that you shouldn’t trigger side effects (i.e. remote operations) in that event handler, since it is executed as part of the component render
method (which should remain pure). So you would have to record the “requirement to save” in your state and then use logic in componentDidUpdate
to execute the remote calls, then change state again when the results arrive.
In my sample, I chose the approach using Redux and redux-saga, which is cleaner in my eyes since it provides a single place to implement the steps of the saving process. My sample implements batch saving, but of course things could be done in a similar way for single-row saving.
Try it!
The sample for this post can be found in the react-frontend branch of my demo project. I recommend you read the readme on that page, and also note that there are instructions now for Running in Cloud 9– so you don’t need any local build environment to check out this and other branches of the demo.