Building a React Data Grid CRUD Admin App 30 Minutes
TL;DR If you’ve built an admin CRUD app before, you know the routine: a grid that’s almost right, a half-finished modal form, a “we’ll do row editing later” sticky note, and three weeks of “filter doesn’t work on date columns” tickets. I rebuilt the classic Northwind admin console in less than 30 minutes — full CRUD across […]
TL;DR
If you’ve built an admin CRUD app before, you know the routine: a grid that’s almost right, a half-finished modal form, a “we’ll do row editing later” sticky note, and three weeks of “filter doesn’t work on date columns” tickets.
I rebuilt the classic Northwind admin console in less than 30 minutes — full CRUD across orders, customers, products, shippers, and salespeople — using the Ignite UI React Data Grid for the UI and a tiny .NET 10 + EF Core SQLite API for the back end. Total UI code: about 1,500 lines of TypeScript. Total time spent fighting the grid: ~0 minutes.
If you’re evaluating a React Data Grid for a real admin app, this is the use case that matters: inline editing, filtering, relational dropdowns, validation, master-detail, summaries, and theming that all hold together once the page stops being a toy.
Repo: react-grids/react-data-grids-crud

That’s the screenshot I want you to walk away with. Hover any row → an action strip slides in with Edit and Delete. Click Edit → the row becomes editable inline, Save/Cancel chips appear. Click into the dropdown → a search-as-you-type combo with grouped headers. Select a row → a master-detail panel opens below it with the joined data.
That’s not a custom build. That’s <IgrGrid rowEditable> plus <IgrActionStrip><IgrGridEditingActions /></IgrActionStrip>. We’ll get to the code in a minute.
The bit nobody talks about: CRUD is hard
Internal admin apps are everywhere. Every company has them. They never make the demo reel. And yet — line for line — they’re some of the hardest UI you can build, because users actually use them every day and notice every rough edge.
A real CRUD page needs:
- Sorting, filtering, paging (sounds easy — try it across date types and locales)
- Inline row editing with proper Save/Cancel semantics and optimistic UX
- A dialog form for creates with rich relational lookups
- Validation that runs before you let the user save
- Master-detail so the user can see context without clicking away
- A delete confirmation that doesn’t feel hostile
- Loading / empty / error states that communicate clearly
- Keyboard accessibility, focus management, screen reader labels
Most teams ship 60% of that, declare victory, and hand it to ops with a shrug. That’s the work the Ignite UI React Data Grid is meant to disappear.
The setup (the boring half — but it has to be there)
The back end is pure boilerplate. Five entities, one EF Core DbContext, five tiny CRUD controllers, Swashbuckle for OpenAPI:
// server/Program.cs
builder.Services.AddDbContext<AppDbContext>(opts =>
opts.UseSqlite("Data Source=northwind.db"));
builder.Services.AddControllers();
builder.Services.AddSwaggerGen();
The controllers are exactly what you’d guess:
// server/Controllers/ProductsController.cs
[ApiController, Route("api/[controller]")]
public class ProductsController(AppDbContext db) : ControllerBase
{
[HttpGet] public Task<List<Product>> GetAll() => db.Products.AsNoTracking().ToListAsync();
[HttpPost] public async Task<Product> Create(Product p) { db.Add(p); await db.SaveChangesAsync(); return p; }
[HttpPut("{id:int}")]public async Task Update(int id, Product p) { db.Update(p); await db.SaveChangesAsync(); }
[HttpDelete("{id:int}")] public async Task Delete(int id) { db.Remove(await db.Products.FindAsync(id)); await db.SaveChangesAsync(); }
}
dotnet run and you get a Swagger UI at /swagger. Now you can see — and call — every endpoint without writing a frontend yet.
This is the contract that makes the React side easy: a typed REST API the frontend can mirror line for line.
The frontend service layer mirrors the API
I don’t reach for OpenAPI codegen for an app this size — the surface is small enough that hand-rolled types stay clearer:
// client/src/api/types.ts
export interface Product {
productID: number;
productName: string;
unitPrice: number;
stockLevel: number;
}
// client/src/api/services.ts
export const ProductsService = {
list: () => api.get<Product[]>('/api/Products'),
get: (id: number) => api.get<Product>(`/api/Products/${id}`),
create: (p: Omit<Product, 'productID'>) => api.post<Product>('/api/Products', p),
update: (p: Product) => api.put<void>(`/api/Products/${p.productID}`, p),
remove: (id: number) => api.del(`/api/Products/${id}`),
};
Five lines per entity. Done. The TypeScript types match the JSON shape ASP.NET serializes by default, so there’s no mapping layer to maintain.
The part this whole post is about: the React Data Grid
Here is the entire CRUD wiring for the Products page. Read it once — there’s nothing hidden:
import {
IgrGrid, IgrColumn, IgrPaginator,
IgrActionStrip, IgrGridEditingActions,
} from 'igniteui-react-grids';
<IgrGrid
data={filtered}
primaryKey="productID"
rowEditable={true}
allowFiltering={true}
filterMode="excelStyleFilter"
moving={true}
onRowEditDone={async (e: CustomEvent) => {
const row = (e.detail as { newValue?: Product })?.newValue;
if (!row) return;
try {
await ProductsService.update(row);
toast.success(`Product "${row.productName}" saved.`);
products.refetch();
} catch (err) {
toast.error(`Save failed: ${(err as Error).message}`);
}
}}
onRowDeleted={async (e: CustomEvent) => {
const row = (e.detail as { data?: Product })?.data;
if (!row) return;
try {
await ProductsService.remove(row.productID);
toast.success(`Deleted "${row.productName}".`);
products.refetch();
} catch (err) {
toast.error(`Delete failed: ${(err as Error).message}`);
}
}}
>
<IgrColumn field="productID" header="ID" editable={false} />
<IgrColumn field="productName" header="Product" editable={true} hasSummary />
<IgrColumn field="unitPrice" header="Price"
dataType="number" editable={true}
bodyTemplate={(c) => <span>{currency(c.cell.value)}</span>} />
<IgrColumn field="stockLevel" header="Stock"
dataType="number" editable={true} hasSummary />
<IgrPaginator perPage={10} />
<IgrActionStrip>
<IgrGridEditingActions addRow={false} />
</IgrActionStrip>
</IgrGrid>
That gives you, in one component:
- Sortable columns (click the header)
- Excel-style column filters (the menu in each header)
- Paging (15/30/45 per page) with a footer paginator
- Drag-to-reorder columns
- Numeric & string summaries (avg, sum, count) in the footer
- Row hover → action strip with Edit / Delete icons
- Click Edit → cells become editable, Save/Cancel chips appear at the bottom of the row
onRowEditDonefires with the merged new row → PUT it to the APIonRowDeletedfires when the user confirms delete → DELETE it from the API
The piece I want to call out is rowEditable={true} plus the IgrActionStrip + IgrGridEditingActions combo. You don’t track edit state yourself. No isEditing flag, no “dirty cells” map, no “did the user press Escape” handler. The React Data Grid manages it; you just listen for the commit and push it to your service.
The other piece is the render-as-you-want bodyTemplate. I’m formatting unitPrice as currency in the example above. Same hook lets you render status pills, avatars, badges, color-coded stock levels — anything React can render — without giving up sort or filter on the underlying value.
Relational lookups belong in a Combo, not a number input
Inline editing is great for primitives, but if your row has foreign keys (salesPersonID, productID, shipperID), you don’t want users typing IDs. You want a searchable, grouped dropdown.
Enter IgrCombo:
<IgrCombo
data={salespeople.map((s) => ({
id: s.salesPersonID,
name: fullName(s.salesPersonFirstName, s.salesPersonLastName),
subtitle: s.salesPersonTitle ?? '',
}))}
valueKey="id"
displayKey="name"
groupKey="subtitle" // ← grouped headers come for free
singleSelect
value={form.salesPersonID != null ? [form.salesPersonID] : []}
onChange={(e) => {
const arr = (e.detail as { newValue?: unknown[] }).newValue ?? [];
setForm((f) => ({ ...f, salesPersonID: (arr[0] as number) ?? null }));
}}
/>
That groupKey="subtitle" line is 90% of why users don’t groan when they see a dropdown:

Bonus features I haven’t shown: type-ahead filtering, multi-select with chip rendering, virtualization for thousands of items, and a slot for custom item templates. Combine IgrCombo with IgrDatePicker and IgrInput and you’ve got the whole “rich create form” story handled by stock components.
Theming — one import, done
The whole app uses the Material Light theme, applied with one CSS import per file:
// main.tsx import 'igniteui-webcomponents/themes/light/material.css'; import 'igniteui-react-grids/grids/themes/light/material.css';
Every Ignite UI surface — chevrons, the action strip, the edit pencil, paginator buttons, dialog headers — picks up the theme automatically. There are four design systems out of the box (Material, Fluent, Indigo, Bootstrap) and dark variants for each. Swap one CSS import and the whole app re-themes.
I also used the Ignite UI MCP servers
I built this using the Ignite UI MCP servers for components and theming. Instead of hand-wiring every page from scratch, I used prompting to generate and refine the app structure, component usage, and visual setup in about 20 minutes total.
If you want to see exactly how it was done, the prompt is in the GitHub repo here: docs/PROMPTS.md. The MCP setup I used is the Ignite UI CLI MCP plus the theming MCP workflow.
What I didn’t have to build
To make the comparison concrete — here’s what I didn’t have to write:
- A virtualized table renderer
- Row editing state management
- Excel-style column filter UI
- A multi-select grouped combo box with type-ahead
- A date picker that renders a calendar
- Cross-browser scroll syncing
- Keyboard navigation across cells
- Focus management for the edit dialog
- A status-pill renderer with proper semantic colors
- Toast / snackbar plumbing
- Consistent visual styling across all of the above
That’s the value prop. The Ignite UI React Data Grid is shaped exactly like the work you don’t want to do twice.
Try it
The full source is at github.com/react-grids/react-data-grids-crud. Clone it, run the .NET API, run the Vite dev server, and you’ll have a working CRUD console in two terminals.

If you want to use the React Data Grid in your own app, start with the Ignite UI React Data Grid page and grab a free trial from there.
The grid I used here is the commercial igniteui-react-grids package. There’s also a free IgrGridLite in the MIT-licensed igniteui-react/grid-lite if you don’t need row editing, master-detail, or the action strip.
The next post in this series will walk through swapping themes at runtime, hooking up a server-side OData endpoint for a million-row dataset, and adding IgrPivotGrid for “orders by salesperson by month” reports.
FAQ
What is the fastest way to build a CRUD admin app in React?
The fastest path is to start with a React Data Grid and form components that already handle the hard parts: editing, filtering, paging, lookups, validation, and accessibility. In this project, the Ignite UI React Data Grid plus a small typed service layer removed most of the custom plumbing that usually slows CRUD work down.
What is a React Data Grid, and when do you need one?
A React Data Grid is a high-function table component built for interactive, data-heavy screens. You need one when your app moves beyond read-only rows and starts requiring inline editing, advanced filtering, summaries, keyboard navigation, virtualization, or relational data workflows.
Can you build a real React CRUD app with inline editing in minutes?
Yes, actually, you can do this in minutes. This demo includes orders, customers, products, shippers, and salespeople with inline row editing, create dialogs, delete confirmation, master-detail, and relational dropdowns in less than 30 minutes using the Ignite UI CLI and Claude Code or GitHub Copilot.
Why use Ignite UI for React for CRUD apps instead of building grid behavior by hand?
Because the expensive part of CRUD apps is not rendering rows. It’s the interaction model around those rows: edit lifecycle, filtering UX, keyboard behavior, grouped lookups, summaries, theming, and consistent state transitions. Ignite UI ships those behaviors as product features, so the code stays focused on business data and API calls.
Does Ignite UI for React work with a .NET Web API backend?
Yes. In this example, the frontend talks to a .NET 10 + EF Core SQLite API over standard REST endpoints. The client code is just typed TypeScript service functions calling /api/Products, /api/Orders, and the other CRUD routes.
Did you build this with the Ignite UI MCP servers too?
Yes. The app uses Ignite UI components at runtime, and the build workflow also benefited from the Ignite UI CLI MCP plus the theming MCP setup. That combination helps AI tools scaffold pages, answer component questions, and keep theming decisions aligned with Ignite UI patterns.
Is Ignite UI React Data Grid free?
The full igniteui-react-grids package used in this post is commercial. If you only need a lighter grid, Infragistics also offers the free IgrGridLite in the MIT-licensed igniteui-react/grid-lite package, though advanced CRUD features like row editing and action strips are part of the commercial offering.
What features matter most in a production CRUD data grid?
The baseline is sorting, filtering, paging, and editing. In practice, teams also need relational lookups, validation, delete confirmation, keyboard accessibility, master-detail context, loading and empty states, and theming that does not fall apart when you add more pages. Those are the features that tend to decide whether an internal tool feels reliable or fragile.
Tags: #react #react-data-grid #typescript #crud #datagrid #ignite-ui #dotnet #admin-ui #mcp