Missed JavaScript Days 2026? The Replays Are Now Available – Watch Now!

How to Use the Ext JS Grid in React: Setup, Columns, Editing and Grouping

October 6, 2026 124 Views

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.

How to Use the Ext JS Grid in React: Setup, Columns, Editing and Grouping

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.

Sencha CTA Banner: Try Sencha ReExt

Author

Team Sencha

Team Sencha is a team of software developers, technology experts, and product specialists with deep experience in building and delivering enterprise-grade web applications. Through our articles, we share practical insights, technical expertise, and industry perspectives to help developers and businesses build better, faster, and more scalable applications with Sencha technologies.

Recommended Articles

React Data Grid: The Complete 2026 Guide for Enterprise Apps

Almost every business application eventually becomes a grid. Orders, invoices, trades, tickets, inventory, patients, shipments: the data your users care about most usually lands in…

Understanding Ext JS Layouts and Containers: An Introduction (Classic vs. Modern)

Building complex, responsive, and data-intensive Web applications requires a solid layout engine. In Ext JS, the layout system is the backbone of your Application UI.…

AI-Driven Test Grading with Ext JS, Node.js, and OpenAI: A Secure Enterprise Architecture

Learn how to build an AI-driven test grading system with Ext JS, Node.js, MySQL, and OpenAI using secure API architecture, deterministic scoring, and human oversight.…

Sencha AI Just Got a Lot Bigger: One Chatbot, Every Sencha Product

Sencha’s AI assistant has grown from a focused Ext JS helper into a product-aware chatbot covering the whole Sencha suite. Here’s what’s new and why…

Mastering Ext JS Performance: Scaling Data Grids from 500 to 1,000,000+ Records

Enterprise applications rarely fail because a grid cannot render a few hundred records. The real challenge begins when that same application evolves into a business-critical…

Building High-Performance Real-Time Operational Dashboards with Ext JS and React

Real-time operational dashboards are no longer simply reporting interfaces. In logistics, fleet management, supply chain operations, financial services, and other data-intensive environments, they are part…

View More