Quantcast
Channel: Developer Express Inc.
Viewing all articles
Browse latest Browse all 2370

DevExtreme React Grid - Remote Data - Integrating with "Real World Patterns"

$
0
0

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 sendChangestowards 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.

Click here to return to the blog series overview


Viewing all articles
Browse latest Browse all 2370

Trending Articles