@bomjan133/react-crud-ui
v1.2.16
Published
A comprehensive React CRUD and UI component library with Zustand store integration
Maintainers
Readme
@bomjan133/react-crud-ui
A comprehensive React CRUD and UI component library with Zustand store integration, layout components, and CLI code generator.
Features
- 🎨 20+ UI Components - Button, Card, Badge, Dialog, Select, Tabs, Table, and more
- 📊 CRUD Components - DataTable, CrudList, CrudForm, CrudDetail with store integration
- 🔄 Zustand Store Factory - Create CRUD stores with minimal code
- 🎯 Form Components - QInput, QSelect, QSwitch, QSearchSelect with validation
- 🏗️ Layout Components - AuthLayout, LoginPage, DashboardLayout with Sidebar & Header
- 📱 Responsive - Mobile-first design with Tailwind CSS v4
- 🌙 Dark Mode - Built-in dark mode support via CSS variables
- ⚡ TypeScript Ready - Full TypeScript support
- 🛠️ CLI Generator - Generate CRUD modules with one command
Documentation
The full component/API reference ships inside the package (node_modules/@bomjan133/react-crud-ui/docs/) and is also inlined below as collapsible sections, so you can read it directly on this page (npmjs.com or GitLab) without leaving it.
- Getting Started - Installation and setup
- UI Components - Button, Card, Badge, Input, Select, etc.
- CRUD Components - DataTable, CrudList, CrudForm, CrudDetail
- Form Components - QInput, QTextarea, QSelect, QSwitch
- Layout Components - AuthLayout, LoginPage, DashboardLayout
- Store Factory - createCrudStore and store patterns
- Book Viewer - BookViewer, EpubViewer, PdfViewer for EPUB/PDF flipbooks
- Toast Notifications - useToast, ToastContainer
- Utilities - formatCurrency, formatDate, debounce, slugify, and more
A comprehensive React CRUD and UI component library with Zustand store integration.
Table of Contents
Installation
npm install @bomjan133/react-crud-ui
# or
yarn add @bomjan133/react-crud-ui
# or
pnpm add @bomjan133/react-crud-uiPeer Dependencies
Make sure you have these peer dependencies installed:
npm install react react-dom react-router-dom zustand axiosQuick Setup
1. Import Styles
Add the styles to your main entry file (e.g., main.jsx or App.jsx):
import '@bomjan133/react-crud-ui/styles.css'2. Configure Tailwind v4 (Optional)
If you're using Tailwind CSS v4 in your project:
index.css:
@import "tailwindcss";
@theme {
/* Light theme - Clean white/gray palette */
--color-background: #ffffff;
--color-foreground: #09090b;
--color-card: #ffffff;
--color-card-foreground: #09090b;
--color-popover: #ffffff;
--color-popover-foreground: #09090b;
--color-primary: #000000;
--color-primary-foreground: #fef2f2;
--color-secondary: #f4f4f5;
--color-secondary-foreground: #18181b;
--color-muted: #f4f4f5;
--color-muted-foreground: #71717a;
--color-accent: #f4f4f5;
--color-accent-foreground: #18181b;
--color-destructive: #ef4444;
--color-destructive-foreground: #fafafa;
--color-border: #e4e4e7;
--color-input: #e4e4e7;
--color-ring: #000000;
/* Sidebar specific */
--color-sidebar: #fafafa;
--color-sidebar-foreground: #3f3f46;
--color-sidebar-primary: #000000;
--color-sidebar-primary-foreground: #fafafa;
--color-sidebar-accent: #f4f4f5;
--color-sidebar-accent-foreground: #18181b;
--color-sidebar-border: #e5e7eb;
/* Border radius */
--radius-sm: 0.25rem;
--radius-md: 0.375rem;
--radius-lg: 0.5rem;
}3. Set up API Client
Create an axios client for API calls:
src/lib/apiClient.js:
import axios from 'axios';
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000/api/v1';
const apiClient = axios.create({
baseURL: API_BASE_URL,
headers: { 'Content-Type': 'application/json' },
});
// Add auth token to requests
apiClient.interceptors.request.use((config) => {
const token = localStorage.getItem('auth_token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// Handle auth errors
apiClient.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
localStorage.removeItem('auth_token');
window.location.href = '/login';
}
return Promise.reject({
message: error.response?.data?.error || error.message,
status: error.response?.status,
data: error.response?.data,
});
}
);
export default apiClient;Project Structure
Recommended project structure when using this package:
src/
├── App.jsx # Main app component
├── main.jsx # Entry point
├── index.css # Global styles (Tailwind)
│
├── pages/ # Page components (by module)
│ ├── auth/
│ │ └── LoginPage.jsx
│ ├── dashboard/
│ │ └── DashboardPage.jsx
│ ├── users/
│ │ ├── UserListPage.jsx
│ │ ├── UserFormPage.jsx
│ │ └── UserDetailPage.jsx
│ └── products/
│ ├── ProductListPage.jsx
│ ├── ProductFormPage.jsx
│ └── ProductDetailPage.jsx
│
├── routes/
│ ├── index.jsx # Main router
│ ├── guards.jsx # Protected/Public route guards
│ └── modules/
│ ├── userRoutes.js
│ └── productRoutes.js
│
├── stores/
│ ├── createCrudStore.js # Import from package or customize
│ ├── useAuthStore.js # Auth state
│ ├── useUIStore.js # UI state (toasts, modals, sidebar)
│ ├── useUserStore.js
│ └── useProductStore.js
│
├── components/
│ └── layouts/
│ ├── MainLayout.jsx # Uses DashboardLayout from package
│ └── AuthLayout.jsx # Uses AuthLayout from package
│
├── hooks/
│ └── useUI.js # Toast, modal, sidebar hooks
│
└── lib/
├── apiClient.js # Axios instance
└── utils.js # Utility functionsCreating Your First CRUD Module
Method 1: Using the CLI (Recommended)
npx @bomjan133/react-crud-ui generate productThis creates:
src/pages/products/ProductListPage.jsxsrc/pages/products/ProductFormPage.jsxsrc/pages/products/ProductDetailPage.jsxsrc/routes/modules/productRoutes.jssrc/stores/useProductStore.js
Method 2: Manual Setup
1. Create the Store:
// src/stores/useProductStore.js
import { createCrudStore } from '@bomjan133/react-crud-ui';
const defaultFormValues = {
name: '',
description: '',
price: 0,
is_active: true,
};
const useProductStore = createCrudStore('product', '/products/', defaultFormValues);
export default useProductStore;2. Create the List Page:
// src/pages/products/ProductListPage.jsx
import { CrudList, Badge } from '@bomjan133/react-crud-ui';
import useProductStore from '@/stores/useProductStore';
const columns = [
{ key: 'name', label: 'Name', sortable: true },
{ key: 'price', label: 'Price', render: (v) => `$${v}` },
{
key: 'is_active',
label: 'Status',
render: (v) => <Badge variant={v ? 'success' : 'secondary'}>{v ? 'Active' : 'Inactive'}</Badge>,
},
];
export default function ProductListPage() {
const store = useProductStore();
return (
<CrudList
title="Products"
description="Manage products"
createPath="/products/create"
editPath="/products"
store={store}
columns={columns}
searchPlaceholder="Search products..."
getDeleteItemName={(item) => item?.name}
/>
);
}3. Create the Form Page:
// src/pages/products/ProductFormPage.jsx
import { Box } from 'lucide-react';
import { CrudForm, QInput, QTextarea, QSwitch } from '@bomjan133/react-crud-ui';
import useProductStore from '@/stores/useProductStore';
export default function ProductFormPage() {
const { form, setFormField } = useProductStore();
return (
<CrudForm
title="Product"
description="Enter product information"
icon={Box}
listPath="/products"
useStore={useProductStore}
>
<QInput
name="name"
label="Name"
value={form.values.name || ''}
onChange={(val) => setFormField('name', val)}
error={form.errors.name}
placeholder="Enter name"
required
/>
<QTextarea
name="description"
label="Description"
value={form.values.description || ''}
onChange={(val) => setFormField('description', val)}
rows={4}
/>
<QInput
name="price"
label="Price"
type="number"
value={form.values.price || 0}
onChange={(val) => setFormField('price', parseFloat(val))}
/>
<QSwitch
label="Active"
checked={form.values.is_active ?? true}
onChange={(val) => setFormField('is_active', val)}
/>
</CrudForm>
);
}4. Create Routes:
// src/routes/modules/productRoutes.js
import ProductListPage from '@/pages/products/ProductListPage';
import ProductFormPage from '@/pages/products/ProductFormPage';
const productRoutes = [
{ path: '/products', element: <ProductListPage /> },
{ path: '/products/create', element: <ProductFormPage /> },
{ path: '/products/:id/edit', element: <ProductFormPage /> },
];
export default productRoutes;Next Steps
- UI Components Guide - Learn about all available UI components
- CRUD Components Guide - Deep dive into CRUD components
- Form Components Guide - Form fields and validation
- Layout Components Guide - Dashboard and Auth layouts
- Store Factory Guide - Creating and customizing stores
Complete reference for all UI components included in @bomjan133/react-crud-ui.
Table of Contents
- Button
- Card
- Badge
- Input & Label
- Select
- Checkbox & Switch
- Dialog
- Dropdown Menu
- Tabs
- Table
- Avatar
- Tooltip
- Date Picker
- Rich Text Editor
- Separator
- Skeleton
- Alert
Button
import { Button, buttonVariants } from '@bomjan133/react-crud-ui';
// Variants
<Button>Default</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="destructive">Destructive</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="link">Link</Button>
<Button variant="success">Success</Button>
// Sizes
<Button size="sm">Small</Button>
<Button size="default">Default</Button>
<Button size="lg">Large</Button>
<Button size="icon"><Icon /></Button>
// With icon
<Button>
<Plus className="h-4 w-4 mr-2" />
Add Item
</Button>
// Loading state
<Button disabled>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
Loading...
</Button>
// As link using buttonVariants
<a href="/path" className={buttonVariants({ variant: "outline" })}>
Link Button
</a>Card
import {
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
CardFooter
} from '@bomjan133/react-crud-ui';
<Card>
<CardHeader>
<CardTitle>Card Title</CardTitle>
<CardDescription>Card description text</CardDescription>
</CardHeader>
<CardContent>
<p>Card content goes here</p>
</CardContent>
<CardFooter className="flex justify-end gap-2">
<Button variant="outline">Cancel</Button>
<Button>Save</Button>
</CardFooter>
</Card>Badge
import { Badge, badgeVariants } from '@bomjan133/react-crud-ui';
// Variants
<Badge>Default</Badge>
<Badge variant="secondary">Secondary</Badge>
<Badge variant="destructive">Destructive</Badge>
<Badge variant="outline">Outline</Badge>
<Badge variant="success">Success</Badge>
<Badge variant="warning">Warning</Badge>
<Badge variant="info">Info</Badge>
// Status badges pattern
const StatusBadge = ({ status }) => {
const variants = {
active: 'success',
pending: 'warning',
inactive: 'secondary',
error: 'destructive',
};
return <Badge variant={variants[status]}>{status}</Badge>;
};Input & Label
import { Input } from '@bomjan133/react-crud-ui';
import { Label } from '@bomjan133/react-crud-ui';
// Basic input
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
placeholder="Enter email"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
</div>
// With error
<div className="space-y-2">
<Label htmlFor="name">Name</Label>
<Input
id="name"
className="border-destructive"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
<p className="text-sm text-destructive">Name is required</p>
</div>
// Disabled
<Input disabled placeholder="Disabled input" />Select
import {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
} from '@bomjan133/react-crud-ui';
// Basic select
<Select value={value} onValueChange={setValue}>
<SelectTrigger className="w-[200px]">
<SelectValue placeholder="Select option" />
</SelectTrigger>
<SelectContent>
<SelectItem value="option1">Option 1</SelectItem>
<SelectItem value="option2">Option 2</SelectItem>
<SelectItem value="option3">Option 3</SelectItem>
</SelectContent>
</Select>
// With groups
<Select>
<SelectTrigger>
<SelectValue placeholder="Select fruit" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Fruits</SelectLabel>
<SelectItem value="apple">Apple</SelectItem>
<SelectItem value="banana">Banana</SelectItem>
</SelectGroup>
<SelectSeparator />
<SelectGroup>
<SelectLabel>Vegetables</SelectLabel>
<SelectItem value="carrot">Carrot</SelectItem>
<SelectItem value="potato">Potato</SelectItem>
</SelectGroup>
</SelectContent>
</Select>Checkbox & Switch
import { Checkbox } from '@bomjan133/react-crud-ui';
import { Switch } from '@bomjan133/react-crud-ui';
import { Label } from '@bomjan133/react-crud-ui';
// Checkbox
<div className="flex items-center space-x-2">
<Checkbox
id="terms"
checked={checked}
onCheckedChange={setChecked}
/>
<Label htmlFor="terms">Accept terms and conditions</Label>
</div>
// Switch
<div className="flex items-center space-x-2">
<Switch
id="active"
checked={active}
onCheckedChange={setActive}
/>
<Label htmlFor="active">Active</Label>
</div>
// Switch with description
<div className="flex items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5">
<Label>Marketing emails</Label>
<p className="text-sm text-muted-foreground">
Receive emails about new products
</p>
</div>
<Switch checked={marketing} onCheckedChange={setMarketing} />
</div>Dialog
import {
Dialog,
DialogTrigger,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
DialogClose,
} from '@bomjan133/react-crud-ui';
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button>Open Dialog</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Dialog Title</DialogTitle>
<DialogDescription>
This is the dialog description.
</DialogDescription>
</DialogHeader>
<div className="py-4">
{/* Dialog content */}
</div>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline">Cancel</Button>
</DialogClose>
<Button onClick={handleSubmit}>Save</Button>
</DialogFooter>
</DialogContent>
</Dialog>Dropdown Menu
import {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
} from '@bomjan133/react-crud-ui';
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => handleEdit(item)}>
<Edit className="h-4 w-4 mr-2" />
Edit
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleView(item)}>
<Eye className="h-4 w-4 mr-2" />
View
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => handleDelete(item)}
className="text-destructive"
>
<Trash className="h-4 w-4 mr-2" />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>Tabs
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@bomjan133/react-crud-ui';
import { QTabs } from '@bomjan133/react-crud-ui';
// Basic Tabs
<Tabs defaultValue="tab1">
<TabsList>
<TabsTrigger value="tab1">Tab 1</TabsTrigger>
<TabsTrigger value="tab2">Tab 2</TabsTrigger>
<TabsTrigger value="tab3">Tab 3</TabsTrigger>
</TabsList>
<TabsContent value="tab1">Content 1</TabsContent>
<TabsContent value="tab2">Content 2</TabsContent>
<TabsContent value="tab3">Content 3</TabsContent>
</Tabs>
// QTabs - Quick tabs with config
const tabs = [
{ value: 'overview', label: 'Overview', content: <Overview /> },
{ value: 'analytics', label: 'Analytics', content: <Analytics /> },
{ value: 'settings', label: 'Settings', content: <Settings /> },
];
<QTabs tabs={tabs} defaultValue="overview" />Table
import {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
} from '@bomjan133/react-crud-ui';
<Table>
<TableCaption>A list of recent invoices.</TableCaption>
<TableHeader>
<TableRow>
<TableHead>Invoice</TableHead>
<TableHead>Status</TableHead>
<TableHead>Method</TableHead>
<TableHead className="text-right">Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{invoices.map((invoice) => (
<TableRow key={invoice.id}>
<TableCell className="font-medium">{invoice.id}</TableCell>
<TableCell>{invoice.status}</TableCell>
<TableCell>{invoice.method}</TableCell>
<TableCell className="text-right">{invoice.amount}</TableCell>
</TableRow>
))}
</TableBody>
<TableFooter>
<TableRow>
<TableCell colSpan={3}>Total</TableCell>
<TableCell className="text-right">$2,500.00</TableCell>
</TableRow>
</TableFooter>
</Table>Table renders the <table> inside a scrolling wrapper. Pass containerClassName to restyle that wrapper — e.g. containerClassName="overflow-visible" to drop the scroll container so a sticky header can pin to the viewport instead of to the wrapper.
Avatar
import { Avatar, AvatarImage, AvatarFallback } from '@bomjan133/react-crud-ui';
<Avatar>
<AvatarImage src="/avatar.jpg" alt="User" />
<AvatarFallback>JD</AvatarFallback>
</Avatar>
// With size
<Avatar className="h-16 w-16">
<AvatarImage src="/avatar.jpg" alt="User" />
<AvatarFallback className="text-lg">JD</AvatarFallback>
</Avatar>Tooltip
import {
Tooltip,
TooltipTrigger,
TooltipContent,
TooltipProvider
} from '@bomjan133/react-crud-ui';
// Wrap your app with TooltipProvider
<TooltipProvider>
<App />
</TooltipProvider>
// Usage
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon">
<Info className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>This is helpful information</p>
</TooltipContent>
</Tooltip>Date Picker
import { DatePicker, DateRangePicker } from '@bomjan133/react-crud-ui';
// Single date picker
const [date, setDate] = useState();
<DatePicker
date={date}
onDateChange={setDate}
placeholder="Pick a date"
/>
// Date range picker
const [dateRange, setDateRange] = useState();
<DateRangePicker
dateRange={dateRange}
onDateRangeChange={setDateRange}
placeholder="Pick a date range"
/>Rich Text Editor
import { RichTextEditor } from '@bomjan133/react-crud-ui';
const [content, setContent] = useState('');
<RichTextEditor
value={content}
onChange={setContent}
placeholder="Start writing..."
height="300px"
/>Separator
import { Separator } from '@bomjan133/react-crud-ui';
// Horizontal
<div>
<h2>Section 1</h2>
<Separator className="my-4" />
<h2>Section 2</h2>
</div>
// Vertical
<div className="flex h-5 items-center space-x-4">
<span>Item 1</span>
<Separator orientation="vertical" />
<span>Item 2</span>
<Separator orientation="vertical" />
<span>Item 3</span>
</div>Skeleton
import { Skeleton } from '@bomjan133/react-crud-ui';
// Basic skeleton
<Skeleton className="h-4 w-[250px]" />
// Card skeleton
<div className="flex items-center space-x-4">
<Skeleton className="h-12 w-12 rounded-full" />
<div className="space-y-2">
<Skeleton className="h-4 w-[250px]" />
<Skeleton className="h-4 w-[200px]" />
</div>
</div>
// Table skeleton
<div className="space-y-2">
{[1, 2, 3, 4, 5].map((i) => (
<Skeleton key={i} className="h-10 w-full" />
))}
</div>Alert
import {
Alert,
AlertTitle,
AlertDescription,
AlertWithIcon,
alertVariants
} from '@bomjan133/react-crud-ui';
// Basic alert
<Alert>
<AlertTitle>Heads up!</AlertTitle>
<AlertDescription>
You can add components to your app using the cli.
</AlertDescription>
</Alert>
// Destructive alert
<Alert variant="destructive">
<AlertTitle>Error</AlertTitle>
<AlertDescription>
Your session has expired. Please log in again.
</AlertDescription>
</Alert>
// With icon
<AlertWithIcon type="success" title="Success" message="Changes saved successfully" />
<AlertWithIcon type="error" title="Error" message="Something went wrong" />
<AlertWithIcon type="warning" title="Warning" message="This action cannot be undone" />
<AlertWithIcon type="info" title="Info" message="New features available" />Complete reference for CRUD components in @bomjan133/react-crud-ui.
Table of Contents
DataTable
A data table with sorting, pagination, search, row selection, clickable "link" columns, and a per-row dropdown actions menu.
Props
| Prop | Type | Default | Description |
|------|------|---------|--------------|
| data | array | [] | Rows to display (current page only — this component does not paginate client-side). |
| columns | array | [] | Column definitions. See Column Definition below. |
| loading | boolean | false | Shows a full-width Loading row instead of the data rows. |
| error | any | null | Accepted but currently unused — nothing in the component reads or renders this value. |
| pagination | boolean | null | Only meaningful when set to false, which hides the pagination footer entirely. Any other value (including the default null) shows it. Pagination is otherwise driven by the flat props below — it is not an object with { page, totalPages, total }. |
| currentPage | number | 1 | Current page (1-indexed). Used to compute serial numbers and passed through to the internal Pagination component. |
| pageSize | number | 15 | Items per page. Used to compute serial numbers and passed to Pagination. |
| totalItems | number | 0 | Total item count, passed to Pagination. |
| totalPages | number | 1 | Total page count, passed to Pagination. |
| onPageChange | function | - | Passed to Pagination; called with the new page. |
| onPageSizeChange | function | - | Passed to Pagination; called when the page size changes. |
| sortable | boolean | true | Global switch for sorting. A column must also set sortable: true to become sortable. |
| sortColumn | string | null | The field currently sorted on (compared against each column's sortKey/accessor/key). |
| sortDirection | 'asc' \| 'desc' \| null | null | Current sort direction. |
| onSort | function | - | Called as onSort(field, direction) when a sortable header is clicked. Clicking cycles asc → desc → none; on the third click it's called as onSort(null, null). |
| openInNewTab | boolean | false | When a column renders as a link (via onViewLink), opens it in a new tab (target="_blank") instead of the same tab. |
| searchable | boolean | true | Shows/hides the search input in the toolbar. |
| searchValue | string | '' | Initial value only — seeds the component's internal search state on mount. The input is managed internally after that; changing this prop later does not reset the input. |
| onSearchChange | function | - | Called with the search string, debounced 300ms after the user stops typing (fixed delay, not configurable). |
| searchPlaceholder | string | 'Search...' | Placeholder text for the search input. |
| filters | node | - | Extra filter control(s) rendered next to the search box. Accepts a single element or an array/fragment of elements; each is wrapped in a w-[200px] container. |
| toolbarActions | node | - | Content rendered on the right side of the toolbar (e.g. an "Add" button). |
| selectable | boolean | false | Shows a checkbox column for row selection, plus a tri-state select-all checkbox in the header. |
| selectedRows | array | [] | Array of currently-selected row keys (as returned by getRowKey). |
| onSelectionChange | function | - | Called with the new array of selected row keys, on both row and select-all/clear actions. |
Select-all header checkbox
When selectable is set, the header cell renders a tri-state checkbox that reflects the rows on the current page:
| Rows selected on this page | Header state | Clicking it | |---|---|---| | None | unchecked | selects every row on the page | | Some, but not all | indeterminate (a dash, not a tick) | selects every row on the page | | All | checked | deselects every row on the page |
Selections made on other pages are preserved — select-all merges the current page's keys into selectedRows, and deselect-all removes only that page's keys. To wipe the selection entirely, call onSelectionChange([]) yourself (that is what the toolbar's "Clear" button does).
| actions | array | [] | Row-level dropdown menu actions. There is no onEdit/onDelete/onView/showActions prop on DataTable itself — a single actions array drives the whole menu. See Actions Column below. |
| onRowClick | function | - | Called with the row object when a row is clicked (ignored if the click originates from the checkbox or actions cells, which stop propagation). |
| striped | boolean | false | Applies a light background to odd rows. |
| hover | boolean | true | Gates two visual behaviors: shows a pointer cursor on rows when onRowClick is set, and (when onRowClick is not set) highlights selected rows with a tinted background. It does not add a generic hover background by itself. |
| compact | boolean | false | Reduces cell vertical padding. |
| className | string | - | Class for the outer wrapping <div>. |
| emptyTitle | string | 'No data found' | Empty-state title. |
| emptyDescription | string | 'There are no items to display.' | Empty-state description text. |
| emptyAction | string | - | Label for an optional empty-state action button. |
| onEmptyAction | function | - | Handler for the empty-state action button. |
| getRowKey | function | (row) => row.id \|\| row._id | Derives a unique key per row; used for React keys and selection tracking. |
| showSerialNumber | boolean | true | Shows a leading row-number column. |
| serialNumberHeader | string | 'SN' | Header label for the serial number column. |
| onViewClick | function | - | If set, "clickable" columns (see clickableColumns) render their value as a button; clicking it calls onViewClick(row). |
| onViewLink | function | - | If set, "clickable" columns render their value as an <a>; called as onViewLink(row) to compute the href. Takes precedence over onViewClick. |
| onBeforeNavigate | function | - | Only used when onViewLink is set and openInNewTab is false. If provided, clicking the link is intercepted (preventDefault/stopPropagation) and onBeforeNavigate(href) is called instead of navigating. |
| clickableColumns | array | [] | List of column accessor/key strings that should render as links/buttons. Pass ['*'] to make every column clickable. If left empty, only columns whose accessor/key is "name" or "title" are clickable by default. |
| enableTextClamping | boolean | true | Truncates long string cell values (30+ characters) with an ellipsis and shows the full text in a tooltip on hover. |
| defaultMaxLines | number | 1 | Default number of lines before clamping; overridable per-column via column.maxLines. |
| tableClassName | string | - | Class passed to the root <Table>. |
| tableHeaderClassName | string | - | Class passed to <TableHeader>. |
| tableBodyClassName | string | - | Class passed to <TableBody>. |
| tableRowClassName | string | - | Class merged onto every data/status <TableRow>. |
| tableHeadClassName | string | - | Class merged onto every <TableHead> cell (selection, SN, data, and actions headers). |
| tableCellClassName | string | - | Class merged onto every <TableCell> (selection, SN, data, actions, and loading/empty cells). |
| headerRowClassName | string | - | Class merged onto the header <TableRow>. |
| statusRowClassName | string | - | Class merged onto the loading/empty-state <TableRow>. |
| paginationClassName | string | - | Class passed to the Pagination component. |
| stickyPagination | boolean | false | Pins the pagination bar to the bottom of the viewport while the table scrolls, so page controls stay reachable on long pages (e.g. the default 15 rows). |
| stickyPaginationClassName | string | - | Class on the sticky pagination wrapper — use it to offset the bar (bottom-16) or restyle its surface. |
| stickyHeader | boolean | false | Pins the column headers to the top of the viewport while the table scrolls. Enabling this removes the table's horizontal scrolling (see note below). |
| stickyHeaderOffset | number \| string | auto | Distance from the top of the viewport where the header parks. A number is treated as pixels. Left unset it reads --rcu-header-offset, which DashboardLayout sets to its own header height — so the table header parks below the app header instead of behind it. Falls back to 0. |
| stickyHeaderClassName | string | - | Class merged onto every pinned <th> — use it to change the pinned background or z-index. |
Not implemented:
onEdit,onDelete,onView,showActions, andemptyMessagedo not exist onDataTable— they were fabricated in an earlier version of this doc. Useactions(below) andemptyTitle/emptyDescriptioninstead.
stickyHeaderand horizontal scrolling are mutually exclusive. Astickyelement pins to its nearest scrolling ancestor, so while the table sits inside a horizontally scrollable wrapper the header can only pin to that wrapper — never to the window. Turning onstickyHeadertherefore drops the wrapper'soverflow-auto, and a table wider than its container will be clipped rather than scrollable. LeavestickyHeaderoff for wide tables, or give the columns narrowerwidth/maxWidthvalues first.
Column Definition
const columns = [
{
// Value extraction — accessor wins over key
accessor: 'name', // string, OR a function: (row) => value
key: 'name', // fallback for accessor; also used as React key/sort field if accessor is absent
// Header text — header wins over label
header: 'Name',
label: 'Name', // fallback if header is not set
sortable: true, // enables sorting for this column (requires top-level sortable !== false)
sortKey: 'name', // optional override of the field name used for sort comparisons/onSort
render: (value, row, index) => (
<span className="font-medium">{value}</span>
), // custom cell renderer, highest precedence
format: (value, row) => value, // used only if render is not provided
width: 200, // inline style: width
minWidth: 120, // inline style: minWidth
maxWidth: 300, // inline style: maxWidth (cells default to '300px' if unset)
headerClassName: '', // extra className on the <TableHead>
cellClassName: '', // extra className on the <TableCell>
disableClamping: false, // opt this column out of enableTextClamping
maxLines: 1, // per-column override of defaultMaxLines
},
];A column-level
classNamefield does not exist — the real per-column style hooks areheaderClassNameandcellClassName.
Actions Column
Passing a non-empty actions array is what causes the Actions column (header cell + per-row dropdown trigger) to render — there is no separate showActions toggle.
const actions = [
{
label: 'Edit',
icon: PencilIcon, // optional component, rendered at 16px with right margin
onClick: (row) => handleEdit(row),
disabled: false, // optional
},
{
label: 'Delete',
icon: TrashIcon,
variant: 'destructive', // only 'destructive' has special styling (red text)
onClick: (row) => handleDelete(row),
hidden: (row) => row.locked, // boolean, or a function(row) => boolean
},
];- Items render in a
DropdownMenuopened by an icon-only ghost button, in the same order as theactionsarray. - Per row, an action is filtered out of the menu if
hiddenistrue, or ifhiddenis a function returningtruefor that row. disabledmaps directly to the underlying menu item's disabled state.- Clicking a row's actions cell does not trigger
onRowClick(click propagation is stopped).
Usage
import { DataTable } from '@bomjan133/react-crud-ui';
import { PencilIcon, TrashIcon } from 'lucide-react';
<DataTable
data={users}
columns={[
{ key: 'name', label: 'Name', sortable: true },
{ key: 'email', label: 'Email' },
{
key: 'status',
label: 'Status',
render: (value) => <Badge>{value}</Badge>,
},
]}
loading={isLoading}
searchValue={search}
onSearchChange={setSearch}
currentPage={page}
pageSize={15}
totalItems={total}
totalPages={Math.ceil(total / 15)}
onPageChange={setPage}
sortColumn={sortColumn}
sortDirection={sortDirection}
onSort={(field, direction) => {
setSortColumn(field);
setSortDirection(direction);
}}
actions={[
{ label: 'Edit', icon: PencilIcon, onClick: (row) => openEdit(row) },
{
label: 'Delete',
icon: TrashIcon,
variant: 'destructive',
onClick: (row) => openDelete(row),
},
]}
emptyTitle="No users found"
emptyDescription="Try adjusting your search or add a new user."
/>
// Selectable rows with a clickable "name" column linking to a detail page
<DataTable
data={orders}
columns={[{ key: 'name', label: 'Order' }, { key: 'amount', label: 'Amount' }]}
selectable
selectedRows={selectedIds}
onSelectionChange={setSelectedIds}
onViewLink={(row) => `/orders/${row.id}`}
clickableColumns={['name']}
getRowKey={(row) => row.id}
/>CrudList
A complete list view component that combines DataTable with a header, toolbar (search/create/refresh/export/bulk actions), pagination, row actions (view/edit/delete + custom), and a delete-confirmation modal. It can source its data either from a store object or from direct props — each store capability (items, loading, error, pagination, fetchList, delete, setPage, setPageSize, setFilters, sorting) is read independently via optional chaining and falls back to the matching direct prop if the store doesn't provide it.
Store integration (preferred)
| Prop | Type | Default | Description |
|------|------|---------|--------------|
| store | object | - | A store instance (e.g. a Zustand store). Recognized shape: store.items (array), store.loading (bool), store.error, store.pagination ({ page, pageSize, total, totalPages }), store.fetchList(), store.delete(id), store.setPage(page), store.setPageSize(size), store.setFilters({ search }), store.sortColumn, store.sortDirection, store.setSort(column, direction). None of these are required — missing pieces fall back to the direct props below. |
Note if you're using this package's own
createCrudStore: its pagination fields are flat (page/pageSize/totalItems/totalPages), not a nestedpaginationobject. SinceCrudListspecifically readsstore.pagination, a plaincreateCrudStoreinstance won't provide it and pagination will appear stuck — bridge it yourself, e.g.store={{ ...store, pagination: { page: store.page, pageSize: store.pageSize, total: store.totalItems, totalPages: store.totalPages } }}. See Store Factory → Compatibility for the full compatibility matrix (this also applies toCrudDetail'scurrentItemvs.item, andCrudForm's store mode).
Direct data props (used if store not provided)
| Prop | Type | Default | Description |
|------|------|---------|--------------|
| data | array | - | Row data. Used when store?.items is nullish. |
| loading | boolean | false | Loading state, used when store?.loading is nullish. |
| error | any | null | Error state, used when store?.error is nullish. |
| skipInitialFetch | boolean | false | When a store is provided, skips the automatic call to store.fetchList() on mount. |
Columns configuration
| Prop | Type | Default | Description |
|------|------|---------|--------------|
| columns | array | [] | Column definitions, forwarded to the internal DataTable — see DataTable's Column Definition for the full field list (accessor/key, header/label, sortable, sortKey, render, format, width fields, headerClassName/cellClassName, clamping options). |
Pagination (direct-prop mode)
| Prop | Type | Default | Description |
|------|------|---------|--------------|
| currentPage | number | 1 | Current page, used when no store.pagination value is available. |
| pageSize | number | 15 | Page size fallback. |
| totalItems | number | 0 | Total item count fallback. |
| totalPages | number | 1 | Total page count fallback. |
| onPageChange | function(page) | - | Called when the store has no setPage method. |
| onPageSizeChange | function(size) | - | Called when the store has no setPageSize method. |
There is no
showPaginationprop.CrudListnever passes apaginationprop through toDataTable, andDataTablerenders its pagination footer whenever there is data — so pagination is always shown once you have data.
Sorting
| Prop | Type | Default | Description |
|------|------|---------|--------------|
| sortColumn | string | - | Current sort column, used when the store doesn't expose sortColumn. |
| sortDirection | 'asc' \| 'desc' \| null | - | Current sort direction fallback. |
| onSort | function(column, direction) | - | Called on sort-header click when no store is present. When a store is present, sort clicks instead call store.setSort(column, direction) followed by store.fetchList(), regardless of whether onSort is also passed. |
Search
| Prop | Type | Default | Description |
|------|------|---------|--------------|
| searchValue | string | "" | Controlled search text, used only when the store has no setFilters method. |
| onSearchChange | function(value) | - | Called on search input change when the store has no setFilters method. |
| searchPlaceholder | string | "Search..." | Placeholder text for the search input. |
| searchable | boolean | true | Whether the search input (and debounced store search) is enabled. This is the real prop for what an earlier version of this doc called showSearch, which does not exist. |
| openInNewTab | boolean | false | When true, clickable-column/view links open via window.open(..., "_blank") instead of SPA navigation, and scroll-position save/restore is skipped. |
When a store with setFilters is present, typing in the search box updates internal state, and after a 300ms debounce calls store.setFilters({ search }), resets to page 1, then calls store.fetchList().
Selection
| Prop | Type | Default | Description |
|------|------|---------|--------------|
| selectable | boolean | false | Enables row checkboxes and the tri-state "select all" header checkbox (see Select-all header checkbox). |
| selectedRows | array | [] | Array of currently-selected row keys (as returned by getRowKey). Controlled — the component does not manage this internally. |
| onSelectionChange | function(newSelectedRowKeys: array) | - | Called with the full updated array whenever a row or "select all" checkbox changes. |
| showBulkDelete | boolean | false | Shows a trash icon inside the "N selected" chip. Clicking it opens a confirmation modal. |
| onBulkDelete | function(selectedRowKeys: array) | - | Runs on confirm. If omitted, CrudList falls back to calling the store's delete once per selected key. |
| bulkDeleteLabel | string | "Delete selected" | Tooltip/aria-label on the trash icon. |
Selection is uncontrolled by default — selectable on its own is enough, and CrudList holds the array internally. Pass selectedRows to take control (needed when something outside the table, like an export button, has to read the selection); onSelectionChange still fires either way.
CRUD Actions (direct-prop mode, used if paths not provided)
| Prop | Type | Default | Description |
|------|------|---------|--------------|
| onCreate | function() | - | Called by the Create button when createPath is not set. |
| onView | function(row) | - | Called by the row "View" action when detailPath is not set. |
| onEdit | function(row) | - | Called by the row "Edit" action when editPath is not set. |
| onDelete | function(row): Promise | - | Called on delete confirmation when the store has no delete method. Awaited; success shows a toast, thrown errors show an error toast. |
| onRefresh | function() | - | Called by the Refresh button when the store has no fetchList method. |
| onExport | function() \| React element | - | If a valid React element, it is rendered as-is in the toolbar. If a function, a default "Export" button is rendered that calls it on click. |
Table & button styling overrides
Purely presentational, forwarded straight through to DataTable/toolbar buttons:
| Prop | Type | Description |
|------|------|-------------|
| tableClassName | string | Class on the <Table> element. |
| tableHeaderClassName | string | Class on <TableHeader>. |
| tableBodyClassName | string | Class on <TableBody>. |
| tableRowClassName | string | Class on each <TableRow> (data + status rows). |
| tableHeadClassName | string | Class on each <TableHead> cell. |
| tableCellClassName | string | Class on each <TableCell>. |
| headerRowClassName | string | Class on the header <TableRow>. |
| statusRowClassName | string | Class on the loading/empty status row. |
| paginationClassName | string | Class passed to the Pagination component. |
| stickyPagination | boolean | Pins the pagination bar to the bottom of the viewport while the list scrolls (default false). |
| stickyPaginationClassName | string | Class on the sticky pagination wrapper — use it to offset the bar (bottom-16) or restyle its surface. |
| stickyHeader | boolean | Pins the column headers to the top of the viewport while the list scrolls (default false). Removes the table's horizontal scrolling. |
| stickyHeaderOffset | number \| string | Where the pinned header parks, measured from the top of the viewport; a number means pixels. Defaults to --rcu-header-offset, which DashboardLayout sets to its own header height, else 0. |
| stickyHeaderClassName | string | Class merged onto every pinned <th>. |
| createButtonClassName | string | Class on the Create button. |
| refreshButtonClassName | string | Class on the Refresh button. |
| exportButtonClassName | string | Class on the default Export button. |
| bulkActionButtonClassName | string | Class applied to every bulk-action button. |
Navigation paths
| Prop | Type | Default | Description |
|------|------|---------|--------------|
| createPath | string | - | If set, the Create button navigates here (via react-router's navigate) instead of calling onCreate. Used as-is — no suffix appended. |
| detailPath | string | - | If set, the row "View" action and clickable-name columns navigate to `${detailPath}/${getRowKey(row)}` instead of calling onView. |
| editPath | string | - | If set, the row "Edit" action navigates to `${editPath}/${getRowKey(row)}/edit` instead of calling onEdit. |
Correction: there is no
viewPathprop — the real prop isdetailPath. An earlier version of this doc usedviewPath, which has no effect.
Breadcrumb helper
| Prop | Type | Default | Description |
|------|------|---------|--------------|
| getBreadcrumbName | function(row) | - | Returns a display name passed as location.state.breadcrumbName when navigating to the detail page. |
| customBreadcrumbPaths | object | - | Static breadcrumb path map passed as location.state.customBreadcrumbPaths on create/view/edit navigation, unless getCustomBreadcrumbPath is given. |
| getCustomBreadcrumbPath | function(row) | - | Per-row/per-action function that computes breadcrumb paths, overriding customBreadcrumbPaths when provided. |
| getParentPath | function(row) | - | Returns a parent path string; used to build { [firstPathSegment]: parentPath } (passed as location.state.parentPath) when neither customBreadcrumbPaths nor getCustomBreadcrumbPath produced a result. |
| preserveLocationState | boolean | false | If true, spreads the current location.state into the state object passed to create/view/edit navigations. |
Custom actions
| Prop | Type | Default | Description |
|------|------|---------|--------------|
| additionalActions | array | [] | Extra per-row dropdown actions, inserted between the built-in Edit and Delete actions. Each item: { label, icon?: Component, onClick(row), variant?: 'destructive', disabled?: boolean, hidden?: boolean \| (row) => boolean }. |
| bulkActions | array | [] | Toolbar buttons shown only when selectable is true and at least one row is selected. Each item: { label, icon?: Component, variant?: string (default "outline"), onClick(selectedRowKeys), disabled?: boolean, className?: string }. |
Labels
| Prop | Type | Default | Description |
|------|------|---------|--------------|
| title | string | - | Card title text. Also used verbatim in the delete-success toast: `${title} Deleted successfully` (note: uses title, not itemName). |
| description | string | - | Card description text. |
| createLabel | string | "Create" | Create button label. |
| refreshLabel | string | "Refresh" | Refresh button label. |
| exportLabel | string | "Export" | Default Export button label. |
| viewLabel | string | "View" | Row "View" action label. |
| editLabel | string | "Edit" | Row "Edit" action label. |
| deleteLabel | string | "Delete" | Row "Delete" action label. |
| itemName | string | "item" | Used in the default delete-confirmation title/description and the empty-state text (`No ${itemName}s found`, `Create your first ${itemName} to get started.`). |
Row key
| Prop | Type | Default | Description |
|------|------|---------|--------------|
| getRowKey | function(row) | (row) => row.uuid \|\| row.id \|\| row._id | Derives a unique key/id for each row; used for selection, navigation URLs, delete calls, and React keys. |
Appearance
| Prop | Type | Default | Description |
|------|------|---------|--------------|
| showHeader | boolean | true | Whether to wrap content in a Card with a header (title/description/toolbar). If false (or no title/description), content renders in a plain <div>. |
| showToolbar | boolean | true | Whether to render the toolbar (header actions, bulk actions, export, refresh, create) — independent of showHeader. |
| compact | boolean | false | Passed to DataTable to tighten row padding. |
| className | string | - | Class on the outer Card/div wrapper. |
| filters | node | - | Custom filter control(s) rendered in the DataTable toolbar next to the search box. |
| headerActions | node | - | Arbitrary content rendered at the start of the toolbar, before bulk actions/export/refresh/create. This is the real "custom header actions" slot — children is not read anywhere in CrudList and has no effect. |
| showEditButton | boolean | true | Hides the built-in row "Edit" action when false, even if editPath/onEdit is set. |
| showDeleteButton | boolean | true | Hides the built-in row "Delete" action when false, even if delete is otherwise available. |
| clickableColumns | array | [] | Accessor keys that should render as clickable links/buttons to the detail view. Pass ["*"] for all columns. Forwarded to DataTable. |
Correction:
icon(a title icon) andchildren(custom header actions) do not exist onCrudList— both were fabricated in an earlier version of this doc. UseheaderActionsfor custom toolbar content.
Delete confirmation
| Prop | Type | Default | Description |
|------|------|---------|--------------|
| deleteConfirmTitle | string | - | Overrides the delete modal's title (default: `Delete ${itemName}`). |
| deleteConfirmDescription | string | - | Overrides the modal's description (default asks to confirm deleting the resolved item name). |
| getDeleteItemName | function(item) | - | Returns the display name for the item being deleted (HTML-stripped and truncated to 80 chars internally); falls back to item.name \|\| item.title \|\| itemName. |
There is no
onDeleteSuccesscallback — delete success always shows a hardcoded toast usingtitle(see Labels above).
Usage
Basic (store-based):
import { CrudList } from '@bomjan133/react-crud-ui';
import { useUserStore } from './stores/useUserStore';
const columns = [
{ accessor: 'name', header: 'Name', sortable: true },
{ accessor: 'email', header: 'Email' },
{
accessor: 'createdAt',
header: 'Created',
format: (value) => new Date(value).toLocaleDateString(),
},
];
function UsersPage() {
const store = useUserStore();
return (
<CrudList
store={store}
title="Users"
description="Manage your organization's users"
columns={columns}
createPath="/users/create"
detailPath="/users"
editPath="/users"
itemName="user"
getRowKey={(row) => row.id}
/>
);
}Selection + bulk actions (direct data props):
import { useState } from 'react';
import { CrudList } from '@bomjan133/react-crud-ui';
import { Archive, Trash2 } from 'lucide-react';
const columns = [
{ accessor: 'name', header: 'Name', sortable: true },
{ accessor: 'status', header: 'Status' },
];
function InvoicesPage({ invoices, isLoading }) {
const [selectedRows, setSelectedRows] = useState([]);
const bulkActions = [
{ label: 'Archive', icon: Archive, onClick: (rowKeys) => archiveInvoices(rowKeys) },
{ label: 'Delete', icon: Trash2, variant: 'destructive', onClick: (rowKeys) => deleteInvoices(rowKeys) },
];
return (
<CrudList
data={invoices}
loading={isLoading}
columns={columns}
title="Invoices"
itemName="invoice"
selectable
selectedRows={selectedRows}
onSelectionChange={setSelectedRows}
bulkActions={bulkActions}
onCreate={() => openCreateModal()}
onView={(row) => openDetailModal(row)}
onEdit={(row) => openEditModal(row)}
onDelete={async (row) => deleteInvoice(row.id)}
getRowKey={(row) => row.id}
/>
);
}CrudForm
A form component for create/edit operations. It is dual-mode — which mode renders is decided by a single runtime check: useStore && children (both truthy).
- Store mode (
useStoreandchildrenboth supplied): renderschildrenverbatim inside a<form>, does its own lightweight "required" validation by inspecting each child'sname/required/labelprops, and callsstore.create/store.update.fields,onSubmit,initialData,defaultValues,validationSchema, andcolumnsare all ignored in this mode. - Fields mode (anything else — including
useStorewithoutchildren, orchildrenwithoutuseStore): a self-containedreact-hook-form+ optional Zod form driven by thefieldsarray andonSubmit.useStore,children,onBeforeSubmit,onAfterCreate, andgetBreadcrumbNameare all ignored in this mode —childrenisn't even rendered.
CrudForm unconditionally calls useNavigate()/useParams() (react-router) and useBreadcrumb(), so it must be rendered under a router and a BreadcrumbProvider regardless of which mode you use.
⚠️ Store-shape warning:
CrudForm's store mode expects a store exposingform.values,form.isSubmitting,create(data),update(id, data),resetForm(),setFormErrors(errorsObj), and one offetchAndPopulateForm(id)/fetchById(id)/fetchOne(id). ThecreateCrudStore/createStorefactory shipped in this same package does not produce this shape — it exposesitem,fetchOne,create,update,loading,submitting,error, with noform,resetForm,setFormErrors, orfetchAndPopulateForm. If you're usingcreateCrudStore, use fields mode instead (fields+onSubmit), or hand-write a store matching store mode's expected shape.
Props
| Prop | Type | Default | Description |
|------|------|---------|--------------|
| useStore | function | - | Zustand store hook. Combined with children to trigger store mode. See store-shape warning above. |
| children | node | - | Store mode only. Rendered as-is inside <form> — CrudForm does not clone children or inject value/onChange. Each field is a fully generic controlled component; you must wire value/onChange to the store yourself. CrudForm only recursively reads each descendant's name, required, and label props to run its own "is empty" required-field check — unrelated to validationSchema/Zod. If passed without useStore (or vice versa), it is silently dropped and fields mode renders instead. |
| fields | array | [] | Fields mode only. Array of field descriptors: { name, type, label, placeholder, description, required, disabled, options, className, hidden, colSpan, render, ...rest }. type is one of text \| email \| password \| number \| url \| tel \| textarea \| select \| search-select \| switch \| section \| custom (unknown/omitted types fall back to a text QInput). hidden may be a boolean or (allValues) => boolean. colSpan is 1 \| 2 \| 3 \| 4 \| 'full'. type: 'section' renders a divider using its own title/description/icon. type: 'custom' calls field.render({ form, field, error, value }). |
| defaultValues | object | {} | Fields mode only. Initial react-hook-form values, used when initialData is not provided. |
| validationSchema | Zod schema | - | Fields mode only. Passed to zodResolver for react-hook-form validation. No effect in store mode. |
| initialData | object | null | Fields mode only. When present, seeds and re-syncs the form (reset()) — the fields-mode equivalent of "edit" data. Store mode instead fetches edit data itself via fetchById/fetchAndPopulateForm. |
| onSubmit | function(data) | - | Fields mode only. Called with the react-hook-form-validated data. Thrown errors are caught and shown via a toast; there is no built-in success toast in fields mode (unlike store mode) — handle success yourself. |
| onCancel | function | - | Store mode: called by the Cancel button (after resetForm()); if omitted, Cancel falls back to the back-navigation handler. Fields mode: the Cancel button is only rendered if onCancel is truthy, and clicking it calls onCancel directly with no fallback. |
| onBack | function | - | Store mode: the back-arrow button (when showBack is true) always calls a handleBack() that uses onBack if given, else navigate(listPath), else navigate(-1). Fields mode: the back-arrow button is only rendered if showBack && onBack are both truthy, and calls onBack directly (no listPath/navigate(-1) fallback). |
| onBeforeSubmit | function(values) => values | - | Store mode only. Called synchronously (not awaited) with the current store.form.values right before submit. Its return value directly replaces the values passed to create/update — there is no "return false to cancel" behavior; it must always return an object. |
| onAfterCreate | function(result, navigate) | - | Store mode only, and only fires after create (never update), and only if create() resolved with a truthy result. If omitted (or the result is falsy), falls back to the back-navigation handler. No equivalent hook exists for the update/edit path. |
| title | string | - | Store mode: form title, auto-prefixed as `Edit ${title}` or `Create ${title}` depending on edit-mode detection (see below), and used in the auto success toast ("{title} created/updated successfully", default "Item" if omitted). Fields mode: rendered as-is (no Edit/Create prefix), and only shown at all if showHeader and (title or description or showBack) are truthy. |
| description | string | - | Subtitle shown under the title, both modes. |
| submitLabel | string | "Save" | Submit button text, both modes. Shows a spinner + "Saving..." while submitting. |
| cancelLabel | string | "Cancel" | Cancel button text, both modes (subject to the onCancel visibility rule above). |
| columns | 1 \| 2 \| 3 \| 4 | 1 | Fields mode only. Grid column count for laying out fields. No effect in store mode. |
| showHeader | boolean | true | Fields mode only. Gates whether the Card+header wrapper is used (vs. a plain <div>). No effect in store mode (its header is unconditional). |
| showFooter | boolean | true | Shows/hides the Cancel/Submit button row, both modes. |
| showBack | boolean | true | Shows the back-arrow icon button. See the asymmetric per-mode behavior under onBack. |
| showCard | boolean | true | Only honored in store mode (wraps output in Card vs. a plain <div>). In fields mode, Card-vs-div is decided solely by showHeader && (title \|\| description \|\| showBack) — this prop is read but never checked there. |
| listPath | string | - | Fallback destination for handleBack() when no onBack is given, in both modes. |
| icon | component | - | Only rendered in store mode when showCard is false. Not used in the Card-wrapped store header, and not used anywhere in fields mode. |
| className | string | - | Applied to the outer Card/div wrapper, both modes. |
| getBreadcrumbName | function(values) | - | Store mode only, active only while editing (uuid route param present). Called with store.form.values once they look populated; its return value is registered via useBreadcrumb().setBreadcrumbName(uuid, name) and cleared on unmount. Requires an ancestor BreadcrumbProvider. |
| loading | boolean | false | OR'd with the store's own loading (store mode). Disables fields and combines with submitting to disable Submit. |
| submitting | boolean | false | OR'd with the store's own form.isSubmitting (store mode). Disables Cancel/Submit and swaps Submit's label/icon for a spinner + "Saving...". |
Not implemented / dead props: there is no
onSuccesscallback — store mode's success message is a hardcoded toast ("{title} created/updated successfully"); the closest customization hook isonAfterCreate(create only).backLabelis destructured but never rendered anywhere — it has no effect.
Edit-mode detection: create-vs-edit is determined solely by Boolean(useParams().uuid) — CrudForm must be rendered on a route with a literal :uuid param (not :id, and not an id prop). This flag drives the title prefix, whether fetchById/fetchAndPopulateForm/fetchOne runs, and whether submit calls store.update(uuid, values) vs store.create(values). Fields mode ignores this entirely — there, "edit" is just whatever initialData you pass in.
Usage
Store mode (requires a hand-written store matching the shape described in the warning above), rendered on routes like /users/new and /users/:uuid/edit:
import { CrudForm, QInput } from '@bomjan133/react-crud-ui';
function UserForm() {
return (
<CrudForm
title="User"
useStore={useUserFormStore}
listPath="/users"
getBreadcrumbName={(values) => values.name}
onBeforeSubmit={(values) => ({ ...values, email: values.email.trim().toLowerCase() })}
onAfterCreate={(result, navigate) => navigate(`/users/${result.uuid}/edit`)}
>
<QInput
name="name"
label="Name"
required
value={useUserFormStore((s) => s.form.values.name) ?? ''}
onChange={(val) => useUserFormStore.getState().setFormValue('name', val)}
/>
<QInput
name="email"
label="Email"
required
value={useUserFormStore((s) => s.form.values.email) ?? ''}
onChange={(val) => useUserFormStore.getState().setFormValue('email', val)}
/>
</CrudForm>
);
}Fields mode (self-contained react-hook-form, no store, no router param required — pass initialData for edit, omit it for create):
import { CrudForm } from '@bomjan133/react-crud-ui';
<CrudForm
title="User"
description="Basic account details"
fields={[
{ name: 'email', type: 'email', label: 'Email', required: true },
{
name: 'role',
type: 'select',
label: 'Role',
options: [
{ value: 'admin', label: 'Admin' },
{ value: 'member', label: 'Member' },
],
},
]}
initialData={editingUser} // omit for create mode
columns={2}
onSubmit={async (data) => {
await api.saveUser(data);
toast.success('Saved');
}}
onCancel={() => navigate('/users')}
onBack={() => navigate('/users')}
/>CrudDetail
A detail-view component for a single record. It supports two distinct rendering modes, and can source its data either from a Zustand-style store hook (useStore) or directly via a data prop.
Mode selection is automatic and based on children:
- Store + render-prop mode — used only when both
useStoreis passed andchildrenis a function. Renders whateverchildren(data)returns inside a plain header + content wrapper (no Card). - Fields-based mode — used in every other case (no
useStore, oruseStorewithout a functionchildren, orchildrenthat isn't a function). Renders aCardwith a field grid built from thefieldsarray.childrenis completely ignored in this mode.
useStore can also be used purely for fetching/deleting without triggering render-prop mode, as long as children isn't a function — data then comes from the store and renders via fields.
⚠️
childrenis a render-prop function, not a plain node, and only takes effect when `us
