Telerik blogs
React

There is a lot you can do with the KendoReact Data Grid. To get an introduction into commonly used features and great ideas about how it can be added to your projects today, read on.

Updated in Aug 2021: This article is a great starting point to get to know the KendoReact Data Grid. We’ve reviewed it to ensure everything is still correct, the code runs and is ready for you to play with. All examples have been updated to use the latest KendoReact Grid version, as of Aug’21.

Since the initial release of the Grid, we have added numerous new features and improvements, such as sticky (frozen) columns, row and column virtualization and multiple selection modes to name just a few. For a tour of the Grid’s complete feature set, head on to the KendoReact Grid overview video by KendoReact PM Carl Bergenhem. And if you learn best by doing, check out this video tutorial on how to implement the Data Grid.

The  KendoReact Data Grid (or Data Table, depends on what you're used to) is one of the most popular components from our React UI component library. It makes a lot of sense why this is the case, many developers are tasked with displaying data within their applications and what better way to present said data than in a tabular fashion?

There's more than just displaying data in rows and columns though. This native UI component, which is a part of our native UI component library built from the ground-up for React (which means zero dependencies), has a ton of features built in to help users organize, modify, and export their data. Some highlights include:

  • Paging
  • Sorting
  • Filtering
  • CRUD Operations
  • Export to PDF and Excel
  • Reordering, resizing and locking (freezing) of columns
  • Virtualization

And that's not even the full list! For this blog post I wanted to take a look at some of the more popular features and how you can implement them, sticking to paging, sorting, filtering, and grouping. Overall this should give you a great foundation for how you can add the KendoReact Grid in to your applications!

Installing the KendoReact Data Grid

Before I get any further I should cover just how to prepare your project to start using the KendoReact Grid.

First, we should npm install all of the packages that we may need:

npm install --save @progress/kendo-react-grid @progress/kendo-data-query @progress/kendo-react-data-tools @progress/kendo-react-inputs @progress/kendo-react-intl @progress/kendo-react-dropdowns @progress/kendo-react-dateinputs @progress/kendo-drawing @progress/kendo-react-animation @progress/kendo-licensing

We're installing a few packages here, but mainly we're adding in the Grid, all of the KendoReact inputs (like dropdowns and datepickers) as well as the ability to work with our internationalization and globalization packages.

Next, within our component we can import our package module:

// ES2015 module syntax
import { Grid } from '@progress/kendo-react-grid';

Or use the CommonJS format:

// CommonJS format
const { Grid } = require('@progress/kendo-react-grid');

Finally, we should make sure that the component is styled in some way. We have three designs (or themes) that you can use: the Default (our home-grown theme), Bootstrap (v5), and Material themes. For this particular sample we will be using Material, based on the guidelines coming from Material Design and one of the most popular design languages today.

To add in one of our themes all you have to do is another npm install like this one:

npm install --save @progress/kendo-theme-default

Then, to actually use this theme in our application (as in, where we need to reference our CSS) we have a couple of options. For more detail as to exactly what you can do please check out our "Styling & Themes" documentation article, but in this case I did a super simple inclusion of our theme in the header of my index.html:

<link rel="stylesheet" href="https://unpkg.com/@progress/kendo-theme-default@latest/dist/all.css" />

Not necessarily something recommended for production - we cover more real-life scenarios in the article I linked above - but certainly something that will work for this article!

It All Starts with Data

Now that things are installed and imported in our project, let's kick things off with the easiest scenario:

Binding to an Array

Let's say we have the following array in our component's state that we want to show in our KendoReact Data Grid:

state = {
  gridData: [
    { "firstName" : "Clark", "lastName" : "Kent", "heroName" : "Superman"  },
    { "firstName" : "Bruce", "lastName" : "Wayne", "heroName" : "Batman" },
    { "firstName" : "Kendra", "lastName" : "Saunders", "heroName" : "Hawkgirl" },
    { "firstName" : "Diana", "lastName" : "Prince", "heroName" : "Wonder Woman" }
  ]
};

All we really need to do is the following:

<Grid data={this.state.gridData} />

That's it! We end up with a Data Table like this:

React Grid

As we can see the Data Table took all of our fields, automatically created columns for them and displayed them all on a single page. However, there are some things that stick out like the header not necessarily looking that great (just taking the field name), and maybe we want to display the name of our super hero first rather than last. The way that we solve this is by defining a set of columns within the table to represent our data. These columns let us also take over specifics that we may want to do on a column-by-column basis (think customization based on data) down the road.

That's looking much better! Notice how the order of the columns have now changed, and the headers are looking a lot better.

Adding Interactivity into the Mix

Paging

For paging there are a few paths we can take. Since we're going to be working with local data it means that we are responsible for slicing the data down to the proper size that we need for the pages we're dealing with.

What we're going for now is fully taking over paging based on the superhero data that we mentioned above. We're taking this approach just to help step through how paging works within the KendoReact Grid on a basic level. There are many other ways, including making the Grid itself more stateful, or working with libraries like our Data Query framework and even Redux to change things. For more samples you can refer to our paging documentation section.

A couple of things that I want to point out are terms that we use in the configuration of the Grid and paging: skip, take, and total. The names kind of give it away, but lets jump in to each one.

skip aligns with how far in our data array we should go. This would be 0 for our initial page, but if we have a page size of 10 and want to jump to the second page we would now have a skip of 10 to start on the next "page" of data.

take is pretty much our page size. If we set this to 10 it means that every page will have 10 items loaded in it.

total just lets the pager know the total number of items that we are binding to. This helps around the calculation to display "X - Y of Z items" where our total is "Z."

With that in mind the way that we enable paging within the KendoReact Grid is through setting the pageable property to true, and then defining the take and skip options.

In our case we only have four data items, so we can make a page size of two, giving us two total pages. Nothing super exciting, but again this is to give you an idea of how paging works in the Grid. Since take and skip are dealing with our data and keeps the current state of the Grid, let's add them to our component's state, like so:

class App extends React.Component { 
  constructor(props) {
    super(props);
    this.state = {
        gridData: [
          { "firstName" : "Clark", "lastName": "Kent", "heroName" : "Superman" },
          { "firstName": "Bruce", "lastName": "Wayne", "heroName" : "Batman"},
          { "firstName": "Kendra", "lastName": "Saunders", "heroName" : "Hawkgirl"},
          { "firstName": "Diana", "lastName": "Prince", "heroName" : "Wonder Woman"}
        ],
        skip: 0,
        take: 2
    }

So, we skip 0, starting with my first item, and we're only setting up take to be 2 (grabbing just Superman and Batman from our array).

Another piece that we have to do to implement paging is to subscribe to the onPageChange event. This event is in charge of letting us know when the page changes and in which direction (if we're paging forward or backwards). This is really just accomplished through updates to skip and take, but it also gives us a chance to modify our data tied to the Grid to the appropriate page. In our case this can be handled through a simple array.slice(), but in more advanced cases we'd do a little more with our data.

Our case is super simple, all we have to do is update our state's skip and take variables to what the event gives us, which means we end up with this:

this.pageChange = (event) => {
  this.setState({
    skip: event.page.skip,
    take: event.page.take
  })
}

Now we just have to configure the Grid to reflect what we've set up in our component.

<Grid
  data={this.state.gridData.slice(this.state.skip, this.state.take + this.state.skip)}
  pageable={true}
  skip={this.state.skip}
  take={this.state.take}
  total={this.state.gridData.length}
  onPageChange={this.pageChange} >
    <Column field="heroName" title="Super Hero" />
    <Column field="firstName" title="First Name" />
    <Column field="lastName" title="Last Name" />
</Grid>

Note what we did in the data property, as well as what we did with total. The latter is easy, we simply say that the total number of items we have is the length of our data array. What we did with data is just a workaround for us not having a "real" data connection here, rather just a local array. So, we use array.slice() and divide up the array based on the skip and take variables we have in our state.

Here's the resulting Grid, with paging and all!

Sorting

With paging added in, let's see what it takes to work with sorting.

Sorting is pretty easy to set up. First we want to add a "sort" variable to our state, just to keep track of the sorting in the Grid. While we will just sort over a single column, this should be an array since we may want to sort over multiple columns. We will just add sort: [] to our state.

On the actual Grid we want to set the sortable property on the Grid, which needs a GridSortSettings object that can define if we want to sort on single or multiple columns, and if we want to give the user the ability to un-sort (remove sorting). We will keep things simple here, but if you want to dive into this more here's an advanced sample. We'll just set this to true for now ending up with sortable={true} added to our declaration Grid.

Just like with paging, we have to make sure a field option is set within one of our columns binding the Grid to a field in our data.

We should also define the sort property, which will give us the highlighted look of what column is currently sorted as well as the arrows that will appear next to the header text (for ascending or descending sorting). This is the field that we already defined on our state earlier, state.sort.

Finally, we need to subscribe to the onSortChange event. This is just like the paging event where it gives us the opportunity to take what the user is trying to update in terms of sorting and apply it to our data. Since we're doing this locally we can just update the sort order, but if we had another data store method we would just apply the sort manually across our data.

this.shortChange = (event) => {
  this.setState({
    sort: event.sort
  })
}

To give an idea of what this sort variable might look like, a typical sort object can look like this:

sort: [
  { field: "heroName", dir: "asc" }
]

The above is saying that we are sorting on the heroName field, and we're doing so in an ascending fashion. We could technically set this from the start and define a sort order from the beginning, but for now we'll just leave things blank.

To get the fundamentals of sorting, this is all we need to do. However, as organizing the sorting of data can be cumbersome, we can lean on the KendoReact Data Query framework to help here. This has a ton of helpful tools for us, one being orderBy which we'll use here to order our array by our sort order.

Note that this is being used to help deal with the local array as a data store. For more advanced scenarios (using state management etc.) we would not really need the Data Query framework.

We already added this to our project when we first did our npm install but let's import this in our component.

import { orderBy } from '@progress/kendo-data-query';

We can now use orderBy() to do something like this:

const data = [
  { name: "Pork", category: "Food", subcategory: "Meat" },
  { name: "Pepper", category: "Food", subcategory: "Vegetables" },
  { name: "Beef", category: "Food", subcategory: "Meat" }
];

const result = orderBy(data, [{ field: "name", dir: "asc" }]);

console.log(result);

/* output
[
  { "name": "Beef", "category": "Food", "subcategory": "Meat" },
  { "name": "Pepper", "category": "Food", "subcategory": "Vegetables" },
  { "name": "Pork", "category": "Food", "subcategory": "Meat" }
]
*/

As we can see all that's needed is to pass in an array and then the sort object to this function. In our case this means that we need to call orderBy on our original set of data, and then pass in the sort object as well.

data={orderBy(this.state.gridData, this.state.sort)}

However, if we want to also have paging still we need to use array.slice again. This should be done on the result of orderBy, so we can just chain it to the end of our orderBy call.

data={orderBy(this.state.gridData, this.state.sort).slice(this.state.skip, this.state.take +  this.state.skip)}

With all of that configured our Grid setup should look like this:

If we run this code we'll see that we can sort our columns simply by clicking on the header and we get that nice indicator to showcase what direction we're sorting in. We're off to the races here!

Filtering

Next up is data grid filtering. This is pretty much the same setup as we have with sorting above. We need to set up a property that defines that we can offer filtering, we provide binding to our state to let us indicate what is currently being filtered (like the sort order indicator above), and finally work with an event when the filter changes. Our filters are also set up with an object that defines the filter and properties around filtering, like what kind of filter we're applying ("contains" or "starts with" etc.).

We don't necessarily need to add a filter variable to our state, so we'll skip this for now. If we wanted to filter something ahead of time we could do so pretty easily by defining this object to whatever filter we want to apply.

On the actual Data Grid we first set up filterable={true} that will immediately make our filter icons and the filter row appear on top of every column. Then, we set the filter property to be equal to the state variable we defined before, so filter={this.state.filter}.

We then subscribe to our event, onFilterChange, and use this to update our state when a user filters.

this.filterChange = (event) =>  {
  this.setState({
    filter: event.filter
  })
}

As a quick reference, the filter variable is expecting a CompositeFilterDescriptor, which really is just an array of FilterDescriptors along with a logic variable that defines if we are using "and" or an "or" filter. The actual FilterDescriptor is a bit long to go through, so I recommend looking over the documentation article I just linked to in order to see how this is built out manually.

The last part that we have to do is actually modify our data. We're at a point where filtering, sorting, and paging needs to be applied to the Data Table and this can quickly become hard to track. How do we apply the sort order, filter order, and eventually even grouping on our array? Well, there's the manual way and there's the easy way: the KendoReact Data Query process function.

Quick aside: Data Query and the Process() Function

This deserves its own mini-section here as this will save us time and effort when it comes to massaging our data for this sample. As mentioned earlier this is not a requirement to use with the Grid. In fact, many of you will have your own state management already set up. This is something that is super helpful for dealing with data in local arrays or without existing state management. Perfect for this particular sample. Also, depending on where you are in your React journey this might be something that you rely on for your first project or two while working with the KendoReact Grid or some of the other data bound components.

The process() function simply takes in our initial data as its first parameter, and as the second it takes an object that contains the skip, take, sort, filter, and group (more on grouping soon) variables that come from the Grid (or are pre-defined) and applies all of these options on our data set, outputting everything into a DataResult object, which the KendoReact Grid uses to have an idea of current page, filter, sort, and group options.

Since we're not using a state management library like Redux, we'll be relying on this method throughout the rest of the sample. This ultimately saves a ton of time binding to our data bound components like the Grid with a local array, just like we're doing here.

Back to Filtering

Now that we know about the process function we can import this instead of our orderBy function (from the same package).

import { process } from '@progress/kendo-data-query';

Then, in our data prop we just do the following:

data = { process(this.state.gridData, this.state) }

How easy is that? Because we already are defining all of the variables we need in our component's state, we can just pass in this.state without creating a new object! The outcome is the following, which now has us filtering, sorting, and paging across all of our data!

Cleaning Things up a Bit

Before we proceed any further, you may have noticed that it is quite busy in our component right now. We've got all of these settings that have been configured on the Grid, all of the fields on our state, and all of these events that are firing. Just like we used process() to simplify our data binding, could we do the same with setting up our Grid?

Maybe I'm making it too easy to set this up, but the short answer is that yes, it's certainly possible to make things easier! Let's chat about onDataStateChange.

The onDataStateChange event fires every time the state of the Grid changes. This means that all of our calls to onPageChange, onSortChange, onFilterChange (and soon onGroupChange when we get to grouping) can get replaced with one single onDataStateChange subscription instead.

We'll want to use this event, but we may want to take a look around the rest of our code first. Right now we do a lot with setting everything right on the root of our state object. It would be a bit more structured if we define a variable to specifically hold all information relating to the Grid, so let's call this gridStateData and put our skip and take variables there.

this.state = {
  gridStateData: {
    skip: 0,
    take: 2
  }
}

With that, we can move to implementing onDataStateChange with the following:

this.dataStateChange = (event) => {
  this.setState({
    gridStateData: event.data
  });
}

Next, let's make the state of the component a little bit simpler and move our data outside of the state and instead pass it in to our React component, something you'll probably be looking to do even in simpler applications. This will be outside of our component's scope and right above the ReactDOM.render function. Don't forget to add a prop and pass in the data!

const appData = [
  { "firstName" : "Clark", "lastName": "Kent", "heroName" : "Superman" },
  { "firstName": "Bruce", "lastName": "Wayne", "heroName" : "Batman"},
  { "firstName": "Kendra", "lastName": "Saunders", "heroName" : "Hawkgirl"},
  { "firstName": "Diana", "lastName": "Prince", "heroName" : "Wonder Woman"}
];

ReactDOM.render(
    <App gridData={appData} />,
    document.querySelector('my-app')
);

This means that we will have to update the data prop on our Grid to be the following:

data={process(this.props.gridData, this.state.gridStateData)}

Notice how we're calling this.props.gridData here since we're now passing this into the component through a prop.

Another area that we can look into, since we're using process() and onDataStateChange to update and set the state upon every sort, filter, page, and group action, is to eliminate a lot of redundant properties as well.

While we technically have to use things like sort, skip, take, etc. within the Grid - why write them on the Grid when they are readily available within our state.gridStateData? We can use ES6 Spread Operator to pass the whole props object. We just need to add {...this.state.gridStateData} to our Grid's declaration. We end up with this in the end.

<Grid
  data={process(this.props.gridData, this.state.gridStateData)}
  {...this.state.gridStateData}
  filterable={true}
  sortable={true}
  pageable={true}
  onDataStateChange={this.dataStateChange} >
    <Column field="heroName" title="Super Hero" />
    <Column field="firstName" title="First Name" />
    <Column field="lastName" title="Last Name" />
</Grid>

Look at how clean that is in comparison! Just as a reference, here's what we have so far in our component.

Grouping

The last piece we should cover is data grid grouping. There are a couple more things to keep in mind when setting up a group, but starting with how the initial configuration might look ends up being similar to what we've done so far. Much like sorting and filtering, we need to set of our groupable, group, and onGroupChange configuration options. Configuring these will give us the ability to drag-and-drop a header to start grouping, or group on a field initially.

There's another part of grouping that we may not initially think of, and this is the Group Header of any group. This is what lets us provide information about our group which is initially just the value of the field we're grouping on, but what about adding in additional information like aggregates here? Also, this contains the expand and collapse icons, which should be tracked somewhere in our state and consumed by the Grid.

This is why there are two other configuration options we will need to set up: onExpandChange, which fires every time we collapse or expand a group, as well as expandField, which lets us define if an item is expanded or collapsed based on the value of this field.

With that information fresh in our heads, let's go ahead and set up some grouping! First, let's add groupable={true} on our Data Grid. We don't need to define onGroupChange because we use onDataStateChange. Also, group will be defined once we group thanks to the spread operator {..this.state.gridStateData}.

This just leaves the two additional configuration options to set up. Let's set expandField="expanded". the expandField prop is what will check if a data item is expanded or not (will only be added to our group header items) and it doesn't matter that we have not defined this elsewhere, even in our original. The Grid will simply add this if it's not available when we expand or collapse. We can get away with this since we are working with a local array, but other scenarios may call for keeping track of expanded items separately from our original data.

After this we will need to set up onExpandChange to make sure we capture when an item is expanded and update the state accordingly. So, we add onExpandChange={this.expandChange} to our Grid and then set up our this.expandChange function like this:

expandChange = event => {
    const item = event.dataItem;

    if (item.groupId) {
      const collapsedIds = !event.value
        ? [...this.state.collapsedState, item.groupId]
        : this.state.collapsedState.filter(groupId => groupId !== item.groupId);
      this.setState({
        collapsedState: collapsedIds
      });
    }
  };

Looking at the first line in this function, we’re getting the current data item, which is part of the event information. If the item has an existing groupId we are setting the new collapsedState value. If the event value is not truthy, we are setting the new value to the ...this.state.collapsedState, item.groupId. Otherwise, we are using the filter() method to set the collapsedState through this.state.collapsedState.filter(groupId => groupId !== item.groupId).

That should be all we need to add in grouping! What is left is to actually try it out by running our sample and dragging a column header to the area that appears once we set groupable on our Data Grid.

Here's the source code to the finished product, which is a Data Grid that can handle paging, sorting, filtering, and grouping! For fun you can always swap out the way you load data (maybe through a JSON file somewhere) and see that this will still work since we've created a fairly generic setup with this.

But Wait, There's More!

This blog post covered a lot so far, but we just went with a basic set of features. We covered this all through binding to a local array rather than something like Redux (which we can certainly bind to by the way). So, we really just scratched the surface and there's even more the KendoReact Data Grid can do!

Just as a small teaser sample, there's editing, hierarchy, PDF export, Excel export, cell templates, column resizing, column reordering, locked columns and way more! Beyond this there are also customizations that can be done with how we display filters, more around grouping - the list goes on and on.

Overall its impossible to cover everything the KendoReact Data Grid can do in one single blog post since there's so much that can be done with the component. This is really why the KendoReact Data Grid documentation exists, and there's plenty more samples showing what the Grid can do. What we tried to accomplish here, is to provide a "Grid 101" intro into the commonly used features and explore ideas about how KendoReact Grid can be used in your projects!

If you're new to KendoReact, learn more about the  100+ React UI components or start playing with our free 30 day trial.


Carl Bergenhem
About the Author

Carl Bergenhem

Carl Bergenhem was the Product Manager for Kendo UI.

Related Posts

Comments

Comments are disabled in preview mode.