How to Use the Ext JS Grid in React: Setup, Columns, Editing and Grouping
Get a summary of this article:
Set up the Ext JS grid in React with ReExt, then add typed columns, store-backed data, inline editing, grouping and summaries. Full code included.
The Ext JS grid has handled data-heavy enterprise screens for years: typed columns, store-backed data, inline editing, grouping and summaries are all built in. With ReExt, you can use that same grid inside a React app, configured with the Ext JS options you already know and wired to React state through ordinary props and event handlers.
This tutorial walks through using the Ext JS grid in React from an empty project to an editable, grouped orders grid with per-region totals.
What you’ll build
An orders grid that:
- shows typed columns for text, numbers, dates and booleans,
- loads data from an Ext JS store, inline or from an API,
- lets users edit cells with validation,
- groups rows by region with subtotals and a grand total,
- sends the selected row and edited values back to React state.
Prerequisites
- Node.js and npm installed.
- Basic React knowledge (components, useState).
- For production use, a licensed Ext JS version 7.x or later. ReExt is included with an Ext JS license. The quick start below runs a trial of Ext JS 7.9 from a remote server, which is fine for learning but not for licensed development.
Step 1: Create a React project and install ReExt
This tutorial uses Vite. ReExt also has quick-start paths for Next.js with JavaScript or TypeScript.
npm create vite@latest orders-grid -- --template react-swc
cd orders-grid
npm install @sencha/reext@latest
cp node_modules/@sencha/reext/dist/example/vite.config.js vite.config.js
The last command copies ReExt’s example Vite configuration into your project. The package ships with built-in TypeScript declarations, so editors get autocomplete for its components and props.
Step 2: Wrap your app in ReExtProvider
ReExtProvider loads the Ext JS framework and makes it available to every ReExt component in your app. Replace src/main.jsx with:
import React from 'react';
import ReactDOM from 'react-dom/client';
import { ReExtProvider } from '@sencha/reext';
import App from './App';
const ReExtData = {
sdkversion: '7.9.0',
toolkit: 'classic',
theme: 'classic',
rtl: false,
locale: 'en',
debug: false,
urlbase: './',
location: 'remote', // use 'local' with a licensed Ext JS copy
};
const reextkey = 'your-reext-key';
ReactDOM.createRoot(document.getElementById('root')).render(
<ReExtProvider splash={true} ReExtData={ReExtData} reextkey={reextkey}>
<App />
</ReExtProvider>
);
The key settings:
| Setting | What it does |
|---|---|
| sdkversion | The Ext JS version to load (7.x or later) |
| toolkit and theme | classic for the classic toolkit and theme used in this tutorial |
| location | remote loads a trial build from Sencha’s server; local loads your licensed copy from the public folder |
| reextkey | Your trial or license key. When you first run a ReExt app without one, a dialog lets you request a trial key by email |
Switching to your licensed Ext JS. Download Ext JS from the Sencha Support Portal, unzip it, copy the resulting folder (for example ext-7.9.0) into your project’s public folder, and set location: ‘local’.
Step 3: Render your first Ext JS grid in React
Every Ext JS component in ReExt is a single ReExt element. The xtype prop picks the component, and the config prop takes the same configuration object you would pass to that component in Ext JS.
First, add some sample data in src/orders.js:
export const orders = [
{ id: 1, customer: 'Acme Corp', region: 'EMEA', amount: 12500, orderDate: new Date(2026, 8, 14), priority: true, status: 'Open' },
{ id: 2, customer: 'Globex', region: 'APAC', amount: 8200, orderDate: new Date(2026, 8, 18), priority: false, status: 'Shipped' },
{ id: 3, customer: 'Initech', region: 'EMEA', amount: 4300, orderDate: new Date(2026, 8, 21), priority: false, status: 'Closed' },
{ id: 4, customer: 'Umbrella', region: 'AMER', amount: 15800, orderDate: new Date(2026, 8, 23), priority: true, status: 'Open' },
{ id: 5, customer: 'Stark Ind.', region: 'AMER', amount: 9600, orderDate: new Date(2026, 8, 27), priority: false, status: 'Shipped' },
{ id: 6, customer: 'Wayne Ent.', region: 'APAC', amount: 6100, orderDate: new Date(2026, 9, 2), priority: true, status: 'Open' },
];
Then replace src/App.jsx:
import ReExt from '@sencha/reext';
import { orders } from './orders';
export default function App() {
return (
<ReExt
xtype="grid"
style={{ height: 420 }}
config={{
title: 'Orders',
columns: [
{ text: 'Customer', dataIndex: 'customer', flex: 1 },
{ text: 'Region', dataIndex: 'region', width: 120 },
{ text: 'Amount', dataIndex: 'amount', width: 140 },
],
data: orders,
}}
/>
);
}
Run npx vite –open. You now have a working grid with sortable, resizable and reorderable columns. Give the React data grid an explicit height through style, or it may render with no visible rows.
Step 4: Configure grid columns
Each entry in columns maps a field (dataIndex) to a column. Specialized column types handle formatting for you:
columns: [
{ text: 'Customer', dataIndex: 'customer', flex: 1, minWidth: 160 },
{ text: 'Region', dataIndex: 'region', width: 110 },
{
text: 'Amount', dataIndex: 'amount', width: 130,
xtype: 'numbercolumn', format: '$0,000', align: 'right',
},
{
text: 'Order date', dataIndex: 'orderDate', width: 130,
xtype: 'datecolumn', format: 'M j, Y',
},
{ text: 'Priority', dataIndex: 'priority', width: 100, xtype: 'checkcolumn' },
{
text: 'Status', dataIndex: 'status', width: 110,
renderer: (value) => (value === 'Open' ? `<b>${value}</b>` : value),
},
],
The most useful column options:
| Option | What it does |
|---|---|
| flex / width / minWidth | Proportional or fixed sizing |
| xtype | Column type: numbercolumn, datecolumn, checkcolumn, and others |
| format | Display format for number and date columns |
| renderer | A function that returns the cell’s display value or HTML |
| align | Text alignment; right-align numbers |
| sortable / hideable / hidden | Control sorting and the column menu |
| locked | Freezes the column on the left while the rest scrolls horizontally |
The renderer above returns HTML. If a value can come from user input, escape it before inserting it, for example with Ext.String.htmlEncode(value).
Step 5: Load data with an Ext JS store
Passing data directly is fine for demos. Real apps use a store, which gives you field types, sorting, filtering, paging and API loading in one place. Replace data: orders with a store config:
store: {
fields: [
{ name: 'amount', type: 'float' },
{ name: 'orderDate', type: 'date', dateFormat: 'Y-m-d' },
{ name: 'priority', type: 'boolean' },
],
data: orders,
sorters: [{ property: 'orderDate', direction: 'DESC' }],
},
The fields list matters most when data arrives as JSON. With type: ‘date’ and a dateFormat, a string like “2026-09-14” becomes a real date, so the date column formats and sorts it correctly.
Loading from an API
To load from a REST endpoint, swap the inline data for a proxy:
store: {
fields: [
{ name: 'amount', type: 'float' },
{ name: 'orderDate', type: 'date', dateFormat: 'Y-m-d' },
],
autoLoad: true,
pageSize: 100,
remoteSort: true,
remoteFilter: true,
proxy: {
type: 'ajax',
url: '/api/orders',
reader: { type: 'json', rootProperty: 'data', totalProperty: 'total' },
},
},
bbar: { xtype: 'pagingtoolbar', displayInfo: true },
With remoteSort and remoteFilter, the store sends sort and filter parameters to your API instead of processing rows in the browser, which is the right choice once datasets grow large. The paging toolbar at the bottom of the grid uses the grid’s store.
To run code once data arrives, use ReExt’s storeloaded prop on the ReExt element:
storeloaded={(store, cmp) => console.log(`${store.getCount()} orders loaded`)}
Step 6: Add inline editing
Editing takes two pieces: an editing plugin on the grid, and an editor on each column users may change.
plugins: [{ ptype: 'cellediting', clicksToEdit: 1 }],
columns: [
{ text: 'Customer', dataIndex: 'customer', flex: 1, editor: { xtype: 'textfield', allowBlank: false } },
{ text: 'Region', dataIndex: 'region', width: 110 },
{
text: 'Amount', dataIndex: 'amount', width: 130,
xtype: 'numbercolumn', format: '$0,000', align: 'right',
editor: { xtype: 'numberfield', minValue: 0, allowBlank: false },
},
{
text: 'Order date', dataIndex: 'orderDate', width: 130,
xtype: 'datecolumn', format: 'M j, Y',
editor: { xtype: 'datefield', format: 'M j, Y' },
},
{
text: 'Status', dataIndex: 'status', width: 110,
editor: { xtype: 'combobox', store: ['Open', 'Shipped', 'Closed'], forceSelection: true },
},
],
Clicking a cell now opens the right editor: a text field, a number field that rejects negative or empty values, a date picker, or a dropdown. Columns without an editor, such as Region here, stay read-only. Edited cells show a small marker until the change is committed.
Row editing instead of cell editing. If users should edit a whole record at once with Update and Cancel buttons, use { ptype: ‘rowediting’, clicksToEdit: 2 } instead. The column editors stay the same.
Saving edits
The grid fires an edit event after each change. In ReExt, events become on props with the first letter capitalized, so edit becomes onEdit:
onEdit={async (editor, context) => {
const { record, field, value, originalValue } = context;
if (value === originalValue) return;
try {
const res = await fetch(`/api/orders/${record.get('id')}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ [field]: value }),
});
if (!res.ok) throw new Error(`Save failed: ${res.status}`);
record.commit(); // clears the edited-cell marker
} catch (e) {
record.reject(); // restores the original value
}
}}
If your store uses a proxy that supports writing, you can call store.sync() instead and let Ext JS send the changes. To block an edit before it is applied, handle onValidateedit and set context.cancel = true.
Step 7: Group rows and add summaries
Grouping needs a groupField on the store and a grouping feature on the grid. The groupingsummary feature adds a subtotal row under each group, and the summary feature adds a grand total.
store: {
fields: [
{ name: 'amount', type: 'float' },
{ name: 'orderDate', type: 'date', dateFormat: 'Y-m-d' },
],
data: orders,
groupField: 'region',
},
features: [
{ ftype: 'groupingsummary', groupHeaderTpl: '{name} ({rows.length} orders)' },
{ ftype: 'summary', dock: 'bottom' },
],
columns: [
{
text: 'Customer', dataIndex: 'customer', flex: 1,
summaryType: 'count',
summaryRenderer: (value) => `${value} orders`,
},
{ text: 'Region', dataIndex: 'region', width: 110 },
{
text: 'Amount', dataIndex: 'amount', width: 130,
xtype: 'numbercolumn', format: '$0,000', align: 'right',
summaryType: 'sum',
summaryRenderer: (value) => Ext.util.Format.number(value, '$0,000'),
},
// ...other columns
],
The result: orders grouped under EMEA, APAC and AMER headers that users can collapse and expand, with an order count and an amount subtotal for each region, and a grand total docked at the bottom.
A few useful details:
- summaryType accepts sum, count, min, max and average, or a function for custom calculations.
- Users can regroup the data themselves. The grouping feature adds “Group by this field” to each column’s header menu.
- Start collapsed with startCollapsed: true in the feature config, useful when there are many groups.
- Subtotals update live. When a user edits an amount, the region subtotal and grand total recalculate immediately.
Ext is available as a global once ReExtProvider has loaded the React frameworks, which is why the summaryRenderer can call Ext.util.Format.
Step 8: Connect grid events to React state
ReExt gives you three ways to connect the Ext JS grid to React:
| Prop | When it runs | Typical use |
|---|---|---|
| ready={(cmp) => …} | Once, when the grid is created | Keep a reference to the grid to call its methods later |
| on<Event> props, such as onSelectionchange or onEdit | When the grid fires that event | Push selections and edits into React state |
| storeloaded={(store, cmp) => …} | When an async store finishes loading | Show counts, hide loaders |
Any event in the Ext JS documentation works the same way: add on and capitalize the first letter. selectionchange becomes onSelectionchange, and itemdblclick becomes onItemdblclick.
To change the grid from React, keep the component from ready in a ref and call Ext JS methods on it, for example gridRef.current.getStore().loadData(newRows) to replace the data.
The complete example
Here is the full src/App.jsx, combining typed columns, a store, inline editing, grouping with summaries, and React state:
import { useMemo, useRef, useState } from 'react';
import ReExt from '@sencha/reext';
import { orders } from './orders';
const money = (value) => Ext.util.Format.number(value, '$0,000');
export default function App() {
const gridRef = useRef(null);
const [selected, setSelected] = useState(null);
const [lastEdit, setLastEdit] = useState('No edits yet');
// Keep the config stable so the grid isn't reconfigured on every render.
const config = useMemo(() => ({
title: 'Orders',
store: {
fields: [
{ name: 'amount', type: 'float' },
{ name: 'orderDate', type: 'date', dateFormat: 'Y-m-d' },
{ name: 'priority', type: 'boolean' },
],
data: orders,
groupField: 'region',
sorters: [{ property: 'orderDate', direction: 'DESC' }],
},
features: [
{ ftype: 'groupingsummary', groupHeaderTpl: '{name} ({rows.length} orders)' },
{ ftype: 'summary', dock: 'bottom' },
],
plugins: [{ ptype: 'cellediting', clicksToEdit: 1 }],
columns: [
{
text: 'Customer', dataIndex: 'customer', flex: 1, minWidth: 160,
editor: { xtype: 'textfield', allowBlank: false },
summaryType: 'count',
summaryRenderer: (value) => `${value} orders`,
},
{ text: 'Region', dataIndex: 'region', width: 110 },
{
text: 'Amount', dataIndex: 'amount', width: 130,
xtype: 'numbercolumn', format: '$0,000', align: 'right',
editor: { xtype: 'numberfield', minValue: 0, allowBlank: false },
summaryType: 'sum',
summaryRenderer: money,
},
{
text: 'Order date', dataIndex: 'orderDate', width: 130,
xtype: 'datecolumn', format: 'M j, Y',
editor: { xtype: 'datefield', format: 'M j, Y' },
},
{ text: 'Priority', dataIndex: 'priority', width: 100, xtype: 'checkcolumn' },
{
text: 'Status', dataIndex: 'status', width: 110,
editor: { xtype: 'combobox', store: ['Open', 'Shipped', 'Closed'], forceSelection: true },
},
],
}), []);
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 16, padding: 16 }}>
<ReExt
xtype="grid"
style={{ height: 460 }}
config={config}
ready={(cmp) => { gridRef.current = cmp; }}
onSelectionchange={(selModel, records) =>
setSelected(records[0] ? records[0].getData() : null)
}
onEdit={(editor, ctx) => {
setLastEdit(`${ctx.record.get('customer')}: ${ctx.field} changed to ${ctx.value}`);
ctx.record.commit(); // replace with an API call in a real app
}}
/>
<div>
<strong>Selected:</strong>{' '}
{selected ? `${selected.customer}, ${money(selected.amount)} (${selected.status})` : 'none'}
</div>
<div><strong>Last edit:</strong> {lastEdit}</div>
<button onClick={() => gridRef.current?.getStore().clearGrouping()}>
Clear grouping
</button>
</div>
);
}
Troubleshooting and tips
| Problem | Likely cause | Fix |
|---|---|---|
| Grid renders with no visible rows | The grid has no height | Set style={{ height: 400 }} or place it in a flex container with a defined height |
| A trial-key dialog appears | No valid reextkey | Request a trial key from the dialog, or use your license key |
| Works in development, fails with licensed Ext JS | Framework folder not found | Copy the Ext JS folder into public and set location: ‘local’ with the matching sdkversion |
| Dates show as blank or raw strings | Field not typed as a date | Add { name: ‘orderDate’, type: ‘date’, dateFormat: ‘Y-m-d’ } to the store’s fields |
| Grid resets on every React render | A new config object is created each render | Define the config outside the component or wrap it in useMemo |
| Ext is undefined in Next.js | Component runs on the server | Add ‘use client’ at the top of files that use ReExt |
Three habits that keep larger grids fast and maintainable:
- Let the store do the work. Use store sorting, filtering and grouping rather than reshaping arrays in React and passing new data.
- Go server-side as data grows. Turn on remoteSort, remoteFilter and paging once datasets reach many thousands of rows.
- Reuse your Ext JS code. If you already have grids defined with Ext.define, ReExt can load them as custom components, so you don’t need to rewrite their configuration in React.
Frequently asked questions
Can I use the Ext JS grid in React?
Yes. ReExt lets you render the Ext JS grid, and other Ext JS components, inside a React app. You configure it with the same options you use in Ext JS, through the config prop of a ReExt component.
Do I need an Ext JS license to use ReExt?
For production, yes. ReExt is included with an Ext JS license and works with commercial Ext JS versions 7.x and above. A remote trial build is available for evaluation.
Does ReExt work with Next.js and TypeScript?
Yes. ReExt has quick-start setups for Vite, Create React App and Next.js with JavaScript or TypeScript, and the package includes TypeScript declarations.
How do I handle grid events in React?
Add on to the Ext JS event name and capitalize its first letter, then pass a function as a prop. For example, the edit event becomes onEdit, and selectionchange becomes onSelectionchange.
Can I reuse my existing Ext JS grids?
Yes. Custom components defined with Ext.define, including grids and stores, can be loaded in a React app and rendered with their xtype. That makes it possible to move an Ext JS app to React screen by screen instead of rewriting it.
Which Ext JS toolkit does ReExt support?
The current release supports the classic toolkit and theme, and has been tested with Ext JS 7.9.
Next steps
You now have an Ext JS grid in React with typed columns, store-backed data, inline editing, grouping and summaries, all connected to React state. From here, try a remote store with paging, add a row editor, or load one of your existing Ext JS grids as a custom component.
Ready to go further? Start a ReExt trial or read the Ext JS to React migration guide to see how to bring your existing grids into React without a rewrite.
Almost every business application eventually becomes a grid. Orders, invoices, trades, tickets, inventory, patients, shipments:…
Building complex, responsive, and data-intensive Web applications requires a solid layout engine. In Ext JS,…
Learn how to build an AI-driven test grading system with Ext JS, Node.js, MySQL, and…
