Redux Integration - Part 2

This section takes a deeper look at integrating ag-Grid with a Redux store by implementing a feature rich File Browser that uses Tree Data.

Following on from Redux Integration Part 1 we will implement a Redux File Browser to demonstrate how the feature rich ag-Grid can be combined with a Redux store to achieve elegant and powerful grid implementations.

Redux File Browser

This section assumes the reader has already covered Redux Integration Part 1 and is familiar with React and ES6 Javascript features.

Creating our Redux File Store

Like in Part 1, our Redux File Store is created using the createStore factory method from the redux module, and just a single reducer is required:

// store.jsx import {createStore} from 'redux'; import fileReducer from './reducers/fileReducer.jsx'; const initialState = { files: [ {id: 1, filePath: ['Documents']}, {id: 2, filePath: ['Documents', 'txt']}, // more files ... ] }; export default createStore(fileReducer, initialState);

Our file browser will allow users to create, move and delete files and folders. The logic for handling these operations is defined in the fileReducer shown below:

// reducers/fileReducer.jsx export function fileReducer(state = {}, action) { const payload = action.payload; switch (action.type) { case types.NEW_FILE: return { files: [ ...state.files, newFile(state.files, payload.filePath) ] }; case types.MOVE_FILES: return { files: moveFiles(state.files, payload.pathToMove, payload.targetPath) }; case types.DELETE_FILES: return { files: deleteFiles(state.files, payload.pathToRemove) }; default: return state; } } The helper methods used in the reducer are omitted for brevity but can be examined in the code tab provided in the example at the end of this section.

Rather than create action objects directly we shall use the following Action Creators as shown below:

// actions/fileActions.jsx export const actions = { newFile(filePath) { return { type: types.NEW_FILE, payload: {filePath} }; }, moveFiles(pathToMove, targetPath) { return { type: types.MOVE_FILES, payload: {pathToMove, targetPath} }; }, deleteFiles(pathToRemove) { return { type: types.DELETE_FILES, payload: {pathToRemove} }; } };

Adding the Provider Component

Now that we have created our Redux store we need to make it available to our React FileBrowser component. This is achieved through the Provider component from the react-redux project.

In the entry point of our application we wrap the FileBrowser component in the Provider component as shown below:

// index.jsx import React from 'react'; import {render} from 'react-dom'; import {Provider} from 'react-redux'; import store from './store.jsx'; import FileBrowser from './FileBrowser.jsx'; render( <Provider store={store}> <FileBrowser/> </Provider>, document.getElementById('root') );

The Provider accepts the store as property and makes it available via props to all child components.

Binding React and Redux

In order for our File Browser to be updated when the store changes we need to bind it.

This is achieved via the connect function provided by the react-redux project.

import React, {Component} from 'react'; import {connect} from "react-redux"; import {bindActionCreators} from 'redux'; class FileBrowser extends Component { render() { /******* Warning, not a real implementation of render(), eg no html! */ /******* The proper render method is given later. */ // an immutable copy of the file state is accessed using: this.props.files; // the following actions can be dispatched to the store: this.props.actions.newFile(filePath); this.props.actions.deleteFiles(filePath); this.props.actions.moveFiles(movingFilePath, targetPath); } } // map files to this.props from the store state const mapStateToProps = (state) => ({files: state.files}); // bind our actions to this.props const mapDispatchToProps = (dispatch) => ({actions: bindActionCreators(actions, dispatch)}); // connect our component to the redux store export default connect( mapStateToProps, mapDispatchToProps null, { forwardRef: true } // must be supplied for react/redux when using GridOptions.reactNext )(FileBrowser);

In the code above we pass two functions to connect to map the required state (mapStateToProps) and actions (mapDispatchToProps). When binding our actions we are using the bindActionCreators utility which wraps our action creators into a dispatch call.

Now that our stateless FileBrowser component is connected to our Redux store, whenever the file state in the store changes, our component will re-render with the latest file state available in this.props.files.

Adding the Data Table

Now that the Redux store is now connected to our stateless React component, all that remains is to implement the view, which just consists of the ag-Grid Data Table in our File Browser.

Before discussing the grid features in our file browser, here are all of the grid options we are using:

// FileBrowser.jsx render() { return ( <div className="ag-theme-fresh"> <AgGridReact // provide column definitions columnDefs={this.colDefs} // specify auto group column definition autoGroupColumnDef={this.autoGroupColumnDef} // row data provided via props from the file store rowData={this.props.files} // enable tree data treeData={true} // return tree hierarchy from supplied data getDataPath={data => data.filePath} // expand tree by default groupDefaultExpanded={-1} // fit grid columns onGridReady={params => params.api.sizeColumnsToFit()} // provide context menu callback getContextMenuItems={this.getContextMenuItems} // provide row drag end callback onRowDragEnd={this.onRowDragEnd} // enable delta updates deltaRowDataMode={true} // return id required for tree data and delta updates getRowNodeId={data => data.id} // specify our FileCellRenderer component frameworkComponents={this.frameworkComponents}> </AgGridReact> </div> ) }

Tree Data

As the data is implicitly hierarchical, with a parent / child relationship between folders and files, we will use the grids Tree Data feature by setting treeData={true}.

The file structure in the file browser is captured in the state as an array of files, where each array entry contains it's hierarchy in the filePath attribute.

files: [ {id: 1, filePath: ['Documents']}, {id: 2, filePath: ['Documents', 'txt']}, {id: 3, filePath: ['Documents', 'txt', 'notes.txt']}, {id: 4, filePath: ['Documents', 'pdf']}, // more files ... ]

This is supplied to the grid via the callback: getDataPath={data => data.filePath}.

For more details see our documentation on Tree Data. The mechanism for connecting Redux to ag-Grid applies equally to when the Tree Data feature is not used.

Row Data Updates

The initial file state along with all subsequent state updates will be provided to the grid component via rowData={this.props.files} from our Redux file store.

This means the grid does not change the state of the files internally but instead receives the new state from the Redux store!

Context Menu Actions

As shown above, getContextMenuItems={this.getContextMenuItems} supplies a function to the grid to retrieve the context menu items. Here is the implementation:

getContextMenuItems = (params) => { if (!params.node) return []; let filePath = params.node.data ? params.node.data.filePath : []; let deleteItem = { name: "Delete", action: () => this.props.actions.deleteFiles(filePath) }; let newItem = { name: "New", action: () => this.props.actions.newFile(filePath) }; return params.node.data.file ? [deleteItem] : [newItem, deleteItem]; };

Notice that we are simply just dispatching actions to the Redux store here. For example, when the new file menu item is selected: action: () => this.props.actions.newFile(filePath).

It is important to note that nothing will happen inside the grid when a menu item is selected until the Redux store triggers a re-render of the grid component with the updated state.

For more details see our documentation on Context Menu.

Row Drag Action

Just like the context menu above, we supply a callback function to handle dragging rows via: onRowDragEnd={this.onRowDragEnd}. Here is the implementation:

onRowDragEnd = (event) => { if(event.overNode.data.file) return; let movingFilePath = event.node.data.filePath; let targetPath = event.overNode.data.filePath; this.props.actions.moveFiles(movingFilePath, targetPath); };

Once again, dragging rows doesn't directly impact the state of the grid. Instead an action is dispatched to the Redux store using: this.props.actions.moveFiles(movingFilePath, targetPath).

For more details see our documentation on Row Dragging.

Delta Row Updates

One consequence of using Redux is that when part of the state is updated in the store, the entire state is replaced with a new version. Delta Row Updates is designed to work specifically with immutable stores such as Redux to ensure only the rows that have been updated will be re-rendered inside the grid.

The file browser enables this feature using: deltaRowDataMode={true}, along with a required row id using: getRowNodeId={data => data.id}.

This feature can lead to noticeable performance improvements in applications which contain alot of row data. For more details see our documentation on Delta Row Updates.

Custom File Cell Renderer

To make our file browser more realistic we will provide a custom Cell Renderer for our files and folders. This is implemented as a react component as follows:

// FileCellRenderer.jsx import React, {Component} from 'react'; export default class FileCellRenderer extends Component { render() { return ( <div> <i className={this.getFileIcon(this.props.value)}/> <span className="filename">{this.props.value} </div> ); } getFileIcon = (filename) => { return filename.endsWith('.mp3') || filename.endsWith('.wav') ? 'far fa-file-audio' : filename.endsWith('.xls') ? 'far fa-file-excel' : filename.endsWith('.txt') ? 'far fa-file' : filename.endsWith('.pdf') ? 'far fa-file-pdf' : 'far fa-folder'; } }

The Cell Renderer is supplied to the grid through: frameworkComponents={this.frameworkComponents}. Where frameworkComponents is just an object referencing the imported component:

import FileCellRenderer from './FileCellRenderer.jsx'; frameworkComponents = { fileCellRenderer: FileCellRenderer };

The key "fileCellRenderer" is passed by name to the innerRenderer used in the Auto Group Column:

autoGroupColumnDef = { headerName: "Files", rowDrag: true, sort: 'asc', width: 250, cellRendererParams: { suppressCount: true, innerRenderer: "fileCellRenderer" } };

For more details see our documentation on Custom Cell Renderer Components.

Demo - Redux File Browser

Now we are ready to enjoy the fruits of our labour! The completed Redux File Browser with source code is shown below. In this example you can:

  • Right Click on a folder for options to delete the folder or add a new file.
  • Right Click on a file for the option to delete the file.
  • Click and Drag on the move icon to move files and folders.

Conclusion

In this section we extended the Redux file store introduced in Part 1, to support the additional functionality required by the File Browser, while still maintaining a nice separation of concerns allowing our view components to remain stateless and focused on presentational detail.

We also explored numerous powerful grid features that support complex grid implementations with the minimal of effort, while proving to be extremely flexible. These features included:

  • Tree Data
  • Custom Context Menu
  • Row Dragging
  • Delta Row Updates
  • Custom Cell Renderer Components

React Grid Resources


  • Get started with React Grid in 5 minutes in our guide.

  • Browse our React Grid page to discover all major benefits in using ag-Grid React.