npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

antd-crud-table

v0.8.1

Published

<div align="center">

Readme

antd-crud-table

A typed, schema-driven CRUD table for React — built on antd and ProComponents.

Describe your columns once and get a paginated, searchable, sortable table with a create/edit form, delete confirmations, bulk actions and export — wired to static data, a REST API, localStorage, or anything you implement yourself.

npm license React types

Live demo · Storybook · API reference · Changelog


Why

Most table libraries hand you a grid and leave the CRUD to you. This one takes a column schema and derives the whole surface from it — the cell renderer, the form control, the validation, and the value conversion in both directions.

<CrudTable<User, 'id'>
  title="Users"
  rowKey="id"
  columns={[
    { dataIndex: 'name',  title: 'Name',  fieldType: 'string', formConfig: { required: true } },
    { dataIndex: 'email', title: 'Email', fieldType: 'email' },
    { dataIndex: 'joined', title: 'Joined', fieldType: 'date' },
  ]}
  hookConfig={{
    api: {
      baseUrl: '/api',
      endpoints: { list: '/users', create: '/users', update: '/users', remove: '/users' },
    },
  }}
/>

That renders a searchable, sortable, paginated table with a working create/edit form, confirmed deletes, and CSV/JSON/Excel export.

Highlights

  • Strictly typed. dataIndex is bound to keyof T, record ids are T[K], and customRender/transform receive that property's own type. No any in the public API.
  • 20 field types — each one entry in a registry declaring its cell renderer, form control, implied validation and value round-trip.
  • Four data strategies behind one CrudDataSource interface: static, REST, localStorage, or your own. Swap backends without touching your columns.
  • Localized. Every string is overridable, and the table follows the antd ConfigProvider around it.
  • Export to CSV, JSON and Excel, covering the whole filtered result set — with CSV formula injection neutralised.
  • Import from CSV, .xls and .xlsx: pick a file, map headers onto columns, preview with per-row validation, then create the valid rows.
  • Almost dependency-free. The heavy things — antd, React, dayjs — are peers you already have. The only runtime dependency is to-spreadsheet, which reads .xlsx on import.

Installation

npm install antd-crud-table
pnpm add antd-crud-table

Peer dependencies

| Package | Version | |---|---| | react, react-dom | ^18 or ^19 | | antd | ^6.3.6 | | @ant-design/icons | ^6 | | @ant-design/pro-components | ^2.8.10 | | dayjs | ^1.11.13 |

Stylesheet

The build extracts CSS to a separate file, so import it once — importing the component alone leaves the table unstyled:

import 'antd-crud-table/styles.css';

Quick start

import { CrudTable } from 'antd-crud-table';
import type { CrudColumn } from 'antd-crud-table';
import 'antd-crud-table/styles.css';

interface User {
  id: number;
  name: string;
  email: string;
  status: 'active' | 'inactive';
  joined: string;
}

const columns: CrudColumn<User>[] = [
  { dataIndex: 'name', title: 'Name', fieldType: 'string', formConfig: { required: true } },
  { dataIndex: 'email', title: 'Email', fieldType: 'email' },
  {
    dataIndex: 'status',
    title: 'Status',
    fieldType: 'enum',
    enumOptions: {
      active: { text: 'Active', color: 'green' },
      inactive: { text: 'Inactive', color: 'red' },
    },
  },
  { dataIndex: 'joined', title: 'Joined', fieldType: 'date' },
];

export const Users = () => (
  <CrudTable<User, 'id'>
    title="Users"
    rowKey="id"
    columns={columns}
    defaultPageSize={10}
    enableBulkOperations
    hookConfig={{
      api: {
        baseUrl: '/api',
        endpoints: { list: '/users', create: '/users', update: '/users', remove: '/users' },
      },
    }}
  />
);

The second type parameter is the row key. It is what makes ids typed: remove(id) takes a number here, not any.

Data strategies

Every strategy implements the same CrudDataSource<T, K> interface, so the columns and behaviour are identical and only the wiring differs.

Static data

hookConfig={{ staticData: users }}

In-memory, seeded once. Edits persist for the session. Good for demos, fixtures and tests.

REST API

hookConfig={{
  api: {
    baseUrl: '/api',
    endpoints: { list: '/users', create: '/users', update: '/users', remove: '/users' },
  },
}}

Endpoints default to /list, /create, /update and /delete under baseUrl, so a conventional collection needs them stated as above. Paging defaults to current/pageSize, and updates to PUT {update}/:id. For an API that speaks a different dialect, paramNames, methods, serializeRequest and parseResponse cover most of it declaratively — see REST dialect recipes for offset/limit, Django REST Framework, JSON:API and auth.

Failures throw RestError, carrying status and body so you can branch on a 422 rather than parsing a message.

localStorage

hookConfig={{ storageKey: 'my-users', initialData: seed }}

Persists across reloads and stamps createdAt / updatedAt.

Your own

hookConfig={{
  operations: {
    list: async (query) => ({ items: await db.find(query), total: await db.count() }),
    create: async (draft) => db.insert(draft),
    update: async (id, draft) => db.update(id, draft),
    remove: async (id) => db.delete(id),
  },
}}

Omitted operations fail with a message naming what is missing, rather than on undefined. For full control, construct a CrudDataSource and pass it as dataSource.

Columns

interface CrudColumnFor<T, K extends keyof T> {
  dataIndex: K;                                   // must be a real key of T
  title: string;                                  // header, and the form label
  fieldType?: FieldType;                          // defaults to 'string'
  enumOptions?: Record<string, EnumOption>;       // for 'enum'
  customRender?: (value: T[K], record: T) => ReactNode;
  formConfig?: {
    required?: boolean;
    component?: ReactNode;                        // replace the control entirely
    transform?: (value: T[K]) => T[K];            // applied before writing
    rules?: FormRule[];                           // antd validation rules
  };
  fieldEditable?: boolean;                        // default true
  searchable?: boolean;                           // default true
}

CrudColumn<T> is the union across every key of T, so an array annotated CrudColumn<User>[] keeps each column's callbacks bound to its own property type. Naming a property that does not exist on T is a compile error.

Field types

| Type | Cell | Form control | |---|---|---| | string | text | Input | | textarea | truncated text | Input.TextArea | | number | locale-grouped | InputNumber | | money | currency | InputNumber (2 dp) | | percent | percentage | InputNumber with % | | boolean | Yes/No tag | Switch | | enum | coloured tag | Select | | date | formatted datetime | DatePicker | | time | time | TimePicker | | dateRange | start ~ end | RangePicker | | email | mailto: link | Input + email rule | | url | external link | Input + url rule | | password | •••••••• | Input.Password | | rating | Rate | Rate | | progress | Progress | InputNumber | | tags | tag list | tag Select | | image | thumbnail | Input | | color | swatch + hex | ColorPicker | | json | inline code | monospace TextArea | | custom | your customRender | your formConfig.component |

Each type is one entry in fieldRegistry declaring its renderer, control, validation and toFormValue/fromFormValue conversion — so a value survives the round-trip into the edit form and back. Browse them all in the Storybook.

Security note: password values are masked in the table and excluded from exports. url, email and image render only http/https (plus mailto:) targets — a javascript: value renders as inert text rather than a clickable link.

Options

| Prop | Type | Default | Description | |---|---|---|---| | title | string | — | Header, and the export filename | | rowKey | K | — | Identity property | | columns | CrudColumn<T>[] | — | Column definitions | | hookConfig | UseCrudTableOptions<T, K> | — | Data strategy | | defaultPageSize | number | 10 | Rows per page | | enableBulkOperations | boolean | false | Row selection and bulk delete | | enableColumnSettings | boolean | true | Column visibility and density | | enableExport | boolean | true | Export menu entries | | exportScope | 'all' \| 'page' | 'all' | Whole result set, or visible rows | | enableImport | boolean | false | Import menu entry (CSV / .xls / .xlsx) | | importConcurrency | number | 5 | Max simultaneous creates during import | | customActions | (record, actions) => ReactNode[] | — | Extra row controls | | locale | PartialCrudTableLocale | English | String overrides |

Localization

The table follows the antd ConfigProvider around it, so setting your app locale once localizes pagination, date pickers, empty states and the ProTable chrome:

import { ConfigProvider } from 'antd';
import frFR from 'antd/locale/fr_FR';

<ConfigProvider locale={frFR}>
  <CrudTable {...config} />
</ConfigProvider>

With no ConfigProvider in the tree the table supplies English itself — antd components otherwise fall back to their own built-in defaults.

The library's own wording comes from the locale prop. Supply only what you want to change; anything omitted stays English:

<CrudTable
  locale={{
    actions: 'Aktionen',
    edit: 'Bearbeiten',
    delete: 'Löschen',
    create: 'Neu',
    confirmDeleteTitle: 'Wirklich löschen?',
    deleteSelected: (count) => `${count} entfernen`,
  }}
  {...config}
/>

Interpolated strings are functions rather than templates with placeholders, so a translation cannot silently drop a value or reorder its arguments. The full contract is CrudTableLocale; enUS is the exported default.

Export

The toolbar menu writes the whole filtered result set, not just the visible page, using the data source's listAll. The labels state which they will do — Export all as CSV or Export page as CSV when the source cannot list without pagination. Set exportScope: 'page' to opt out.

| Format | Output | |---|---| | csv | .csv, formula-injection safe | | json | .json, the raw records | | excel | .xls, Excel 2003 SpreadsheetML |

Cells beginning =, +, -, @, tab or CR are prefixed so spreadsheets treat them as text. Quoting alone does not prevent evaluation, and row content in a CRUD table is exactly the untrusted input an attacker controls. Genuine numbers are left alone.

Excel note: excel emits SpreadsheetML in a .xls file rather than OOXML, which would mean taking on a ZIP implementation. Excel 2016+ shows a format/extension mismatch prompt on open; the file is intact. Use CSV if you need a prompt-free export.

Import

Set enableImport to add an Import entry to the toolbar menu. It opens a dialog that walks a file in: pick it, map its headers onto columns (auto-matched on header text, and editable), preview the parsed rows with per-row validation using the same formConfig.rules the create form uses, then create the valid rows.

<CrudTable<User, 'id'>
  title="Users"
  rowKey="id"
  columns={columns}
  hookConfig={{ storageKey: 'users' }}
  enableImport
/>

| Format | Read via | |---|---| | .csv | built-in RFC 4180 parser, no dependency | | .xls | the Excel 2003 SpreadsheetML this library exports, parsed as plain XML | | .xlsx | real OOXML, via to-spreadsheet's readExcel |

Behaviour worth knowing:

  • Values round-trip. Export then import reproduces the original records. Enum columns accept both the stored key and the exported label.
  • Partial success is honest. Rows failing validation are skipped and reported per row; a row that fails to create does not abort the rest.
  • Bounded concurrency. A large file does not fire one request per row. A data source may implement the optional createMany(drafts) to create server-side in a batch; otherwise creates run through a small pool (importConcurrency, default 5).
  • Passwords are never imported, the same way they are never exported.

Using the hook directly

useCrudTable works without the table when you want your own UI:

const crud = useCrudTable<User, 'id'>('id', { storageKey: 'users' });

crud.state;                 // { loading, error, data, total, page, pageSize }
await crud.create({ name: 'Ada' });
await crud.update(1, { name: 'Ada L.' });
await crud.remove(1);

onSuccess and onError fire for every operation — including list failures, with the original Error rather than a flattened string.

Documentation

| | | |---|---| | Live demo | Every strategy, running | | Storybook | One story per field type | | API reference | Generated from the types | | REST recipes | Non-default API dialects | | Migration guide | Upgrading from 0.5.x | | Changelog | Release history |

Development

pnpm install
pnpm dev              # demo app
pnpm storybook        # component playground
pnpm test             # unit and component tests
pnpm test:coverage    # with enforced thresholds
pnpm lint
pnpm build:lib        # the published package
pnpm build:site       # demo + storybook + api reference

Contributions are welcome. Tests and lint run on every pull request against both React 18 and 19.

License

MIT © Maifee Ul Asad