@mbcorai/crud-kit
v1.0.0
Published
Reusable CRUD UI toolkit for React and Next.js applications
Maintainers
Readme
@mbcorai/crud-kit
A complete, type-safe CRUD toolkit for React and Next.js applications. Provides reusable components, hooks, and context for building data management interfaces with minimal boilerplate.
Features
- Complete CRUD Operations — Create, Read, Update, Delete with minimal setup
- Type-Safe — Built with TypeScript and Zod validation
- React Context Integration — Global CRUD state management
- Reusable Components — DataTable, Modal, Form, Buttons
- Form Validation — React Hook Form + Zod integration
- Table Rendering — TanStack React Table with sortable columns
- Loading & Error States — Built-in UI components for async operations
- Flexible & Extensible — Customizable actions, columns, and layouts
- Next.js Compatible — Works seamlessly with App Router
Technologies
- React 19+
- TypeScript
- React Hook Form — Form state management
- Zod — Schema validation
- @hookform/resolvers — Zod integration with React Hook Form
- TanStack React Table — Advanced table features
- Tailwind CSS — Styling
Installation
npm install @mbcorai/crud-kitOr with yarn:
yarn add @mbcorai/crud-kitConfiguration
1 — Wrap your app with CrudProvider
import { CrudProvider } from "@mbcorai/crud-kit"
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html>
<body>
<CrudProvider>
{children}
</CrudProvider>
</body>
</html>
)
}2 — Create your data schema with Zod
import { z } from "zod"
export const userSchema = z.object({
firstname: z.string().min(2),
lastname: z.string().min(2),
email: z.email(),
password: z.string().min(6),
age: z.coerce.number().min(1),
role: z.enum(["ADMIN", "CLIENT"]),
})
export type UserFormValues = z.infer<typeof userSchema>3 — Define your API operations
const users = useCrudResource<User>({
// Fetch all records
async getMany() {
const response = await fetch("/api/users")
return response.json()
},
// Create a new record
async create(data) {
const response = await fetch("/api/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
})
return response.json()
},
// Update an existing record
async update(id, data) {
const response = await fetch(`/api/users/${id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
})
return response.json()
},
// Delete a record
async remove(id) {
await fetch(`/api/users/${id}`, { method: "DELETE" })
},
// Callbacks
onSuccess() {
console.log("Operation successful")
},
onError(error) {
console.error("Operation failed:", error)
},
})Using the Components
Basic Example
"use client"
import { useMemo } from "react"
import {
CrudProvider,
useCrudContext,
useCrudResource,
DataTable,
CrudModal,
CrudForm,
InputField,
FormActions,
CreateButton,
EditButton,
DeleteButton,
CrudLoading,
CrudError,
CrudEmpty,
CreateCrudColumn,
} from "@mbcorai/crud-kit"
import { UserFormValues, userSchema } from "@/schemas/user.schema"
function UserPageContent() {
const crud = useCrudContext()
const users = useCrudResource<User>({
async getMany() {
const response = await fetch("/api/users")
return response.json()
},
async create(data) {
const response = await fetch("/api/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
})
return response.json()
},
async update(id, data) {
const response = await fetch(`/api/users/${id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
})
return response.json()
},
async remove(id) {
await fetch(`/api/users/${id}`, { method: "DELETE" })
},
onSuccess() {
crud.resetCrudState()
},
})
const columns = useMemo(
() => [
CreateCrudColumn<User>("firstname", "Firstname"),
CreateCrudColumn<User>("lastname", "Lastname"),
CreateCrudColumn<User>("email", "Email"),
CreateCrudColumn<User>("role", "Role"),
{
id: "actions",
header: "Actions",
cell: ({ row }) => (
<div className="flex gap-2">
<EditButton item={row.original} />
<DeleteButton item={row.original} />
</div>
),
},
],
[crud]
)
async function handleSubmit(data: UserFormValues) {
if (crud.mode === "edit" && crud.selectedItem) {
await users.updateItem(crud.selectedItem.id, data)
crud.closeModal()
return
}
await users.createItem(data)
crud.closeModal()
}
if (users.loading) {
return <CrudLoading />
}
if (users.error) {
return <CrudError message={users.error} />
}
const isEmpty = !users.data.length
return (
<div className="p-10 space-y-6">
<div className="flex justify-between items-center">
<h1 className="text-3xl font-bold">Users</h1>
<CreateButton onClick={() => crud.openModal("create")} />
</div>
{isEmpty ? (
<CrudEmpty message="No users found" />
) : (
<DataTable data={users.data} columns={columns} />
)}
<CrudModal
open={crud.open}
onClose={crud.closeModal}
title={crud.mode === "edit" ? "Edit User" : "Create User"}
>
<CrudForm<UserFormValues>
schema={userSchema}
defaultValues={
crud.selectedItem || {
firstname: "",
lastname: "",
email: "",
password: "",
age: 18,
role: "CLIENT",
}
}
onSubmit={handleSubmit}
>
<InputField name="firstname" label="Firstname" />
<InputField name="lastname" label="Lastname" />
<InputField name="email" label="Email" />
<InputField name="password" label="Password" type="password" />
<InputField name="age" label="Age" type="number" />
<InputField name="role" label="Role" />
<FormActions />
</CrudForm>
</CrudModal>
</div>
)
}
export default function UserPage() {
return (
<CrudProvider>
<UserPageContent />
</CrudProvider>
)
}Architecture & File Structure
Core Concepts
The package follows a layered architecture:
- Context Layer — Manages global CRUD state
- Hooks Layer — Exposes CRUD functionality to components
- Components Layer — Reusable UI components
- Types Layer — TypeScript definitions
File-by-File Breakdown
Context Layer
context/CrudContext.tsx
Provides global CRUD state management using React Context.
Responsibilities:
- Creates a
CrudContextfor storing modal state and selected item - Exports
CrudProviderwrapper component - Exports
useCrudContext()hook for accessing CRUD state anywhere
Key exports:
CrudProvider<T>— Wrapper componentuseCrudContext<T>()— Hook to access modal state, open/close functions
Hooks Layer
hooks/useCrud.ts
The core hook managing modal and selection state.
Responsibilities:
- Manages modal open/close state
- Stores current CRUD mode ("create", "edit", "delete")
- Stores selected item for editing
- Provides functions to open/close modal and reset state
Key exports:
useCrud<T>()— Returns object withopen,mode,selectedItem,openModal(),closeModal(),resetCrudState()
hooks/resources/useCrudResource.ts
The main hook for API operations and data fetching.
Responsibilities:
- Fetches data on mount via
config.getMany() - Manages loading and error states
- Provides
createItem(),updateItem(),removeItem()functions - Calls
config.onSuccess()after mutations - Uses
useRefto prevent infinite re-fetches when config changes
Key exports:
useCrudResource<T>(config: CrudResourceConfig<T>)— Returns object withdata[],loading,error,refresh(),createItem(),updateItem(),removeItem()
Components Layer
components/DataTable.tsx
Renders a table using TanStack React Table.
Responsibilities:
- Renders table headers and rows
- Uses
flexRenderto render column headers and cells - Supports custom column definitions
Props:
data: T[]— Array of items to displaycolumns: ColumnDef<T>[]— Column definitions
components/CrudModal.tsx
Modal dialog for forms (create/edit/delete).
Responsibilities:
- Shows/hides modal based on
openprop - Renders title and close button
- Renders children (typically forms)
Props:
open: boolean— Whether modal is visibleonClose: () => void— Close handlertitle: string— Modal titlechildren: ReactNode— Modal content
components/forms/CrudForm.tsx
Generic form wrapper using React Hook Form + Zod.
Responsibilities:
- Wraps
FormProviderfor form context - Accepts schema for validation
- Provides form methods to children via React Hook Form context
Props:
schema: ZodType<T>— Zod validation schemadefaultValues: Partial<T>— Initial form valuesonSubmit: SubmitHandler<T>— Form submission handlerchildren: ReactNode— Form fields
components/forms/InputField.tsx
Reusable form input component.
Responsibilities:
- Registers field with React Hook Form
- Converts numeric inputs to numbers
- Displays validation errors
- Supports any input type (text, email, password, number, etc.)
Props:
name: string— Field namelabel: string— Display labeltype?: string— HTML input type (default: "text")
components/forms/FormActions.tsx
Renders submit button(s).
Responsibilities:
- Renders a submit button with customizable label
Props:
submitLabel?: string— Button text (default: "Save")
components/actions/CreateButton.tsx
Button to open create form.
Responsibilities:
- Calls
openModal("create")on click - Allows custom
onClickhandler
Props:
label?: string— Button text (default: "create")onClick?: () => void— Custom click handler
components/actions/EditButton.tsx
Button to open edit form for an item.
Responsibilities:
- Calls
openModal("edit", item)on click - Requires the item to edit
Props:
item: T— Item to editlabel?: string— Button text (default: "Edit")
components/actions/DeleteButton.tsx
Button to trigger delete operation.
Responsibilities:
- Calls
openModal("delete", item)on click - Item can then be deleted via API
Props:
item: T— Item to deletelabel?: string— Button text (default: "Delete")
components/states/CrudLoading.tsx
Loading state indicator.
Responsibilities:
- Displays loading message
Props:
message?: string— Loading text (default: "Loading...")
components/states/CrudError.tsx
Error state display.
Responsibilities:
- Displays error message in red
Props:
message: string— Error message (required)
components/states/CrudEmpty.tsx
Empty state display.
Responsibilities:
- Displays "no data" message
Props:
message?: string— Empty text (default: "No data found")
Library Utilities
lib/CreateCrudColumn.ts
Helper function to create standard table columns.
Responsibilities:
- Generates a column definition with accessor key and header
- Handles safe value display (shows "-" for null/undefined)
Function:
CreateCrudColumn<T>(
accessorKey: keyof T,
header: string
): ColumnDef<T>lib/CreateCrudActions.tsx
Helper function to create an actions column.
Responsibilities:
- Generates a column for edit/delete buttons
Function:
CreateCrudActions<T>(
actions: CrudActionsProps<T>
): ColumnDef<T>Types Layer
types/CrudContextTypes.ts
Type definitions for CRUD context.
Exports:
CrudContextType<T>— Context value shape
types/CrudHookTypes.ts
Type definitions for the CRUD hook.
Exports:
CrudMode— "create" | "edit" | "delete"CrudState<T>— State shape with modal state
types/CrudResourceTypes.ts
Type definitions for resource operations.
Exports:
CrudResourceConfig<T>— Configuration object for API operationsCrudResourceReturn<T>— Return object fromuseCrudResource
types/CrudFormTypes.ts
Type definitions for form components.
Exports:
CrudFormProps<T>— Props forCrudFormInputFieldProps— Props forInputFieldFormActionsProps— Props forFormActions
types/CrudActionsTypes.ts
Type definitions for action buttons.
Exports:
CrudActionsProps<T>— Callbacks for edit/delete actionsEditButtonProps<T>— Props forEditButtonDeleteButtonProps<T>— Props forDeleteButton
types/CrudModalTypes.ts
Type definitions for the modal.
Exports:
CrudModalProps— Props forCrudModal
types/DataTableTypes.ts
Type definitions for the data table.
Exports:
DataTableProps<T>— Props forDataTable
Data Flow
App Component
↓
CrudProvider (wraps everything)
↓
useCrudContext() — Access modal state
useCrudResource() — Fetch & manage data
↓
DataTable + CrudModal
↓
CreateButton → openModal("create")
EditButton → openModal("edit", item)
DeleteButton → openModal("delete", item)
↓
CrudForm → Validation via Zod + React Hook Form
↓
handleSubmit() → Call users.createItem() / updateItem() / removeItem()
↓
API → useCrudResource updates state → Table re-rendersState Management Flow
- Initial Load:
useCrudResourcecallsconfig.getMany()on mount - Open Modal:
EditButton/CreateButtoncallscrud.openModal()with item (if editing) - Fill Form: Form fields are populated with
defaultValues - Submit:
handleSubmit()callsusers.createItem()/updateItem()/removeItem() - Refresh: After operation,
useCrudResource.refresh()is called automatically - Close Modal:
crud.closeModal()andcrud.resetCrudState() - Re-render: Table updates with fresh data
Customization
Custom Columns
const customColumn: ColumnDef<User> = {
id: "custom",
header: "Custom",
cell: ({ row }) => <span>{row.original.firstname.toUpperCase()}</span>,
}
const columns = [
...standardColumns,
customColumn,
]Custom Styling
All components use Tailwind CSS classes. Override by wrapping or modifying component classes.
Custom Actions
Use EditButton and DeleteButton in custom column definitions, or create your own buttons calling crud.openModal() or users.removeItem().
Best Practices
- Always wrap your app with
CrudProvider - Use
useMemofor column definitions to prevent unnecessary re-renders - Stabilize API config with
useMemoinuseCrudResource - Use
z.coerce.number()for numeric fields to handle string inputs - Use
valueAsNumberinInputFieldfor numeric input types - Call
crud.resetCrudState()inonSuccesscallback to close modal after operations - Handle errors with
CrudErrorstate component
License
MIT
