DevExtreme v25.2 is now available.

Explore our newest features/capabilities and share your thoughts with us.

Your search did not match any results.

React Data Grid - Batch Update Request

With the DevExtreme DataGrid, users can modify multiple records and submit all changes simultaneously (when editing.mode is set to "batch"). Batch editing allows you to optimize your app, address performance related issues, and deliver the best possible user experience across a variety of usage scenarios.

Backend API
import React from 'react'; import DataGrid, { Column, Editing, Pager } from 'devextreme-react/data-grid'; import type { DataGridRef, DataGridTypes } from 'devextreme-react/data-grid'; import { createStore } from 'devextreme-aspnet-data-nojquery'; import 'whatwg-fetch'; // const BASE_PATH = 'http://localhost:5555'; const BASE_PATH = 'https://js.devexpress.com/Demos/NetCore'; const URL = `${BASE_PATH}/api/DataGridBatchUpdateWebApi`; async function fetchAntiForgeryToken(): Promise<{ headerName: string; token: string }> { try { const response = await fetch(`${BASE_PATH}/api/Common/GetAntiForgeryToken`, { method: 'GET', credentials: 'include', cache: 'no-cache', }); if (!response.ok) { const errorMessage = await response.text(); throw new Error(`Failed to retrieve anti-forgery token: ${errorMessage || response.statusText}`); } return await response.json(); } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; throw new Error(errorMessage); } } async function getAntiForgeryTokenValue(): Promise<{ headerName: string; token: string }> { const tokenMeta = document.querySelector<HTMLMetaElement>('meta[name="csrf-token"]'); if (tokenMeta) { const headerName = tokenMeta.dataset.headerName || 'RequestVerificationToken'; const token = tokenMeta.getAttribute('content') || ''; return Promise.resolve({ headerName, token }); } const tokenData = await fetchAntiForgeryToken(); const meta = document.createElement('meta'); meta.name = 'csrf-token'; meta.content = tokenData.token; meta.dataset.headerName = tokenData.headerName; document.head.appendChild(meta); return tokenData; } const ordersStore = createStore({ key: 'OrderID', loadUrl: `${URL}/Orders`, async onBeforeSend(_method, ajaxOptions) { const tokenData = await getAntiForgeryTokenValue(); ajaxOptions.xhrFields = { withCredentials: true, headers: { [tokenData.headerName]: tokenData.token }, }; }, }); function normalizeChanges(changes: DataGridTypes.DataChange[]): DataGridTypes.DataChange[] { return changes.map((c) => { switch (c.type) { case 'insert': return { type: c.type, data: c.data, }; case 'update': return { type: c.type, key: c.key, data: c.data, }; case 'remove': return { type: c.type, key: c.key, }; default: return c; } }) as DataGridTypes.DataChange[]; } async function sendBatchRequest(url: string, changes: DataGridTypes.DataChange[], headers: Record<string, string>) { try { const response = await fetch(url, { method: 'POST', body: JSON.stringify(changes), headers: { 'Content-Type': 'application/json;charset=UTF-8', ...headers, }, credentials: 'include', }); if (!response.ok) { const errorMessage = await response.text(); throw new Error(`Batch save failed: ${errorMessage || response.statusText}`); } } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; throw new Error(errorMessage); } } async function processBatchRequest(url: string, changes: DataGridTypes.DataChange[], component: ReturnType<DataGridRef['instance']>) { const tokenData = await getAntiForgeryTokenValue(); await sendBatchRequest(url, changes, { [tokenData.headerName]: tokenData.token }); await component.refresh(true); component.cancelEditData(); } const onSaving = (e: DataGridTypes.SavingEvent) => { e.cancel = true; if (e.changes.length) { const changes = normalizeChanges(e.changes); e.promise = processBatchRequest(`${URL}/Batch`, changes, e.component); } }; const App = () => ( <DataGrid id="gridContainer" dataSource={ordersStore} showBorders={true} remoteOperations={true} repaintChangesOnly={true} onSaving={onSaving}> <Editing mode="batch" allowAdding={true} allowDeleting={true} allowUpdating={true} /> <Pager visible={true} /> <Column dataField="OrderID" allowEditing={false}></Column> <Column dataField="ShipName"></Column> <Column dataField="ShipCountry"></Column> <Column dataField="ShipCity"></Column> <Column dataField="ShipAddress"></Column> <Column dataField="OrderDate" dataType="date"></Column> <Column dataField="Freight"></Column> </DataGrid> ); export default App;
import React from 'react'; import DataGrid, { Column, Editing, Pager } from 'devextreme-react/data-grid'; import { createStore } from 'devextreme-aspnet-data-nojquery'; import 'whatwg-fetch'; // const BASE_PATH = 'http://localhost:5555'; const BASE_PATH = 'https://js.devexpress.com/Demos/NetCore'; const URL = `${BASE_PATH}/api/DataGridBatchUpdateWebApi`; async function fetchAntiForgeryToken() { try { const response = await fetch(`${BASE_PATH}/api/Common/GetAntiForgeryToken`, { method: 'GET', credentials: 'include', cache: 'no-cache', }); if (!response.ok) { const errorMessage = await response.text(); throw new Error( `Failed to retrieve anti-forgery token: ${errorMessage || response.statusText}`, ); } return await response.json(); } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; throw new Error(errorMessage); } } async function getAntiForgeryTokenValue() { const tokenMeta = document.querySelector('meta[name="csrf-token"]'); if (tokenMeta) { const headerName = tokenMeta.dataset.headerName || 'RequestVerificationToken'; const token = tokenMeta.getAttribute('content') || ''; return Promise.resolve({ headerName, token }); } const tokenData = await fetchAntiForgeryToken(); const meta = document.createElement('meta'); meta.name = 'csrf-token'; meta.content = tokenData.token; meta.dataset.headerName = tokenData.headerName; document.head.appendChild(meta); return tokenData; } const ordersStore = createStore({ key: 'OrderID', loadUrl: `${URL}/Orders`, async onBeforeSend(_method, ajaxOptions) { const tokenData = await getAntiForgeryTokenValue(); ajaxOptions.xhrFields = { withCredentials: true, headers: { [tokenData.headerName]: tokenData.token }, }; }, }); function normalizeChanges(changes) { return changes.map((c) => { switch (c.type) { case 'insert': return { type: c.type, data: c.data, }; case 'update': return { type: c.type, key: c.key, data: c.data, }; case 'remove': return { type: c.type, key: c.key, }; default: return c; } }); } async function sendBatchRequest(url, changes, headers) { try { const response = await fetch(url, { method: 'POST', body: JSON.stringify(changes), headers: { 'Content-Type': 'application/json;charset=UTF-8', ...headers, }, credentials: 'include', }); if (!response.ok) { const errorMessage = await response.text(); throw new Error(`Batch save failed: ${errorMessage || response.statusText}`); } } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; throw new Error(errorMessage); } } async function processBatchRequest(url, changes, component) { const tokenData = await getAntiForgeryTokenValue(); await sendBatchRequest(url, changes, { [tokenData.headerName]: tokenData.token }); await component.refresh(true); component.cancelEditData(); } const onSaving = (e) => { e.cancel = true; if (e.changes.length) { const changes = normalizeChanges(e.changes); e.promise = processBatchRequest(`${URL}/Batch`, changes, e.component); } }; const App = () => ( <DataGrid id="gridContainer" dataSource={ordersStore} showBorders={true} remoteOperations={true} repaintChangesOnly={true} onSaving={onSaving} > <Editing mode="batch" allowAdding={true} allowDeleting={true} allowUpdating={true} /> <Pager visible={true} /> <Column dataField="OrderID" allowEditing={false} ></Column> <Column dataField="ShipName"></Column> <Column dataField="ShipCountry"></Column> <Column dataField="ShipCity"></Column> <Column dataField="ShipAddress"></Column> <Column dataField="OrderDate" dataType="date" ></Column> <Column dataField="Freight"></Column> </DataGrid> ); export default App;
import React from 'react'; import ReactDOM from 'react-dom'; import App from './App.tsx'; ReactDOM.render( <App />, document.getElementById('app'), );
window.exports = window.exports || {}; window.config = { transpiler: 'ts', typescriptOptions: { module: 'system', emitDecoratorMetadata: true, experimentalDecorators: true, jsx: 'react', }, meta: { 'react': { 'esModule': true, }, 'typescript': { 'exports': 'ts', }, 'devextreme/time_zone_utils.js': { 'esModule': true, }, 'devextreme/localization.js': { 'esModule': true, }, 'devextreme/viz/palette.js': { 'esModule': true, }, 'devextreme-aspnet-data-nojquery': { 'esModule': true, }, 'openai': { 'esModule': true, }, }, paths: { 'npm:': 'https://cdn.jsdelivr.net/npm/', 'bundles:': '../../../../bundles/', 'externals:': '../../../../bundles/externals/', }, defaultExtension: 'js', map: { 'ts': 'npm:plugin-typescript@8.0.0/lib/plugin.js', 'typescript': 'npm:typescript@4.2.4/lib/typescript.js', 'jszip': 'npm:jszip@3.10.1/dist/jszip.min.js', 'react': 'npm:react@17.0.2/umd/react.development.js', 'react-dom': 'npm:react-dom@17.0.2/umd/react-dom.development.js', 'prop-types': 'npm:prop-types/prop-types.js', 'devextreme-aspnet-data-nojquery': 'npm:devextreme-aspnet-data-nojquery@5.0.0/index.js', 'whatwg-fetch': 'npm:whatwg-fetch@2.0.4/fetch.js', 'rrule': 'npm:rrule@2.6.4/dist/es5/rrule.js', 'luxon': 'npm:luxon@3.4.4/build/global/luxon.min.js', 'es6-object-assign': 'npm:es6-object-assign', 'devextreme': 'npm:devextreme@link:../../packages/devextreme/artifacts/npm/devextreme/cjs', 'devextreme-react': 'npm:devextreme-react@link:../../packages/devextreme-react/npm/cjs', 'devextreme-quill': 'npm:devextreme-quill@1.7.8/dist/dx-quill.min.js', 'devexpress-diagram': 'npm:devexpress-diagram@2.2.24/dist/dx-diagram.js', 'devexpress-gantt': 'npm:devexpress-gantt@4.1.64/dist/dx-gantt.js', 'inferno': 'npm:inferno@8.2.3/dist/inferno.min.js', 'inferno-compat': 'npm:inferno-compat/dist/inferno-compat.min.js', 'inferno-create-element': 'npm:inferno-create-element@8.2.3/dist/inferno-create-element.min.js', 'inferno-dom': 'npm:inferno-dom/dist/inferno-dom.min.js', 'inferno-hydrate': 'npm:inferno-hydrate/dist/inferno-hydrate.min.js', 'inferno-clone-vnode': 'npm:inferno-clone-vnode/dist/inferno-clone-vnode.min.js', 'inferno-create-class': 'npm:inferno-create-class/dist/inferno-create-class.min.js', 'inferno-extras': 'npm:inferno-extras/dist/inferno-extras.min.js', '@preact/signals-core': 'npm:@preact/signals-core@1.8.0/dist/signals-core.min.js', 'devextreme-cldr-data': 'npm:devextreme-cldr-data@1.0.3', // SystemJS plugins 'plugin-babel': 'npm:systemjs-plugin-babel@0.0.25/plugin-babel.js', 'systemjs-babel-build': 'npm:systemjs-plugin-babel@0.0.25/systemjs-babel-browser.js', // Prettier 'prettier/standalone': 'npm:prettier@2.8.8/standalone.js', 'prettier/parser-html': 'npm:prettier@2.8.8/parser-html.js', }, packages: { 'devextreme': { defaultExtension: 'js', }, 'devextreme-react': { main: 'index.js', }, 'devextreme-react/common': { main: 'index.js', }, 'devextreme/events/utils': { main: 'index', }, 'devextreme/common/core/events/utils': { main: 'index', }, 'devextreme/localization/messages': { format: 'json', defaultExtension: 'json', }, 'devextreme/events': { main: 'index', }, 'es6-object-assign': { main: './index.js', defaultExtension: 'js', }, }, packageConfigPaths: [ 'npm:@devextreme/*/package.json', ], babelOptions: { sourceMaps: false, stage0: true, react: true, }, }; window.process = { env: { NODE_ENV: 'production', }, }; System.config(window.config); // eslint-disable-next-line const useTgzInCSB = ['openai'];
import React from 'react'; import ReactDOM from 'react-dom'; import App from './App.js'; ReactDOM.render(<App />, document.getElementById('app'));
<!DOCTYPE html> <html lang="en"> <head> <title>DevExtreme Demo</title> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0" /> <link rel="stylesheet" type="text/css" href="https://cdn3.devexpress.com/jslib/25.2.3/css/dx.light.css" /> <link rel="stylesheet" type="text/css" href="styles.css" /> <script src="https://cdn.jsdelivr.net/npm/core-js@2.6.12/client/shim.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/systemjs@0.21.3/dist/system.js"></script> <script type="text/javascript" src="config.js"></script> <script type="text/javascript"> System.import("./index.tsx"); </script> </head> <body class="dx-viewport"> <div class="demo-container"> <div id="app"></div> </div> </body> </html>
#gridContainer { height: 440px; }
using DevExtreme.AspNet.Data; using DevExtreme.AspNet.Mvc; using Newtonsoft.Json; using System; using System.Linq; using System.Net; using System.Net.Http; using System.Web.Http; using DevExtreme.MVC.Demos.Models.Northwind; using DevExtreme.MVC.Demos.Models.DataGrid; using System.Collections.Generic; using ValidateAntiForgeryToken = DevExtreme.MVC.Demos.WebApiValidateAntiForgeryTokenAttribute; namespace DevExtreme.MVC.Demos.Controllers { [Route("api/DataGridBatchUpdateWebApi/{action}", Name = "DataGridBatchUpdateWebApi")] public class DataGridBatchUpdateWebApiController : ApiController { InMemoryNorthwindContext _nwind = new InMemoryNorthwindContext(); [HttpGet] public HttpResponseMessage Orders(DataSourceLoadOptions loadOptions) { return Request.CreateResponse(DataSourceLoader.Load(_nwind.Orders, loadOptions)); } [HttpPost] [ValidateAntiForgeryToken] public HttpResponseMessage Batch(List<DataChange> changes) { foreach(var change in changes) { Order order; if(change.Type == "update" || change.Type == "remove") { var key = Convert.ToInt32(change.Key); order = _nwind.Orders.First(o => o.OrderID == key); } else { order = new Order(); } if(change.Type == "insert" || change.Type == "update") { JsonConvert.PopulateObject(change.Data.ToString(), order); Validate(order); if(!ModelState.IsValid) return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState.GetFullErrorMessage()); if(change.Type == "insert") { _nwind.Orders.Add(order); } change.Data = order; } else if(change.Type == "remove") { _nwind.Orders.Remove(order); } } _nwind.SaveChanges(); return Request.CreateResponse(HttpStatusCode.OK, changes); } } }
using System; using System.Collections.Generic; using System.Data.Entity; namespace DevExtreme.MVC.Demos.Models.Northwind { public class InMemoryNorthwindContext : InMemoryDataContext<Order> { readonly NorthwindContext _nwind = new NorthwindContext(); public DbSet<Customer> Customers => _nwind.Customers; public DbSet<Order_Detail> Order_Details => _nwind.Order_Details; public ICollection<Order> Orders => ItemsInternal; public DbSet<Shipper> Shippers => _nwind.Shippers; protected override IEnumerable<Order> Source => _nwind.Orders; protected override int GetKey(Order item) => item.OrderID; protected override void SetKey(Order item, int key) => item.OrderID = key; } }

If data is stored on a server, our DataGrid sends multiple requests to save edited objects - one request per object (this is because most servers only process one edit operation at a time). If your server supports batch update, you can configure the DataGrid to save all changes with a single request.

To incorporate this functionality into your web app, implement the DataGrid's onSaving function. This function accepts an e object that contains fields used for batch update. The following is a summary of the steps you must follow to enable batch update:

  1. Disable default save logic
    Set the e.cancel field to true.

  2. Send pending changes to the server
    Pending changes are contained in the e.changes array. Ensure it is not empty and send the changes to the server.

  3. Update data in the DataGrid
    Once changes are saved, call the refresh(changesOnly) method.

  4. Reset edit state
    Use the cancelEditData() method to clear pending changes.