CRUD System
useCrudViewModel hook, GenericCrudView, DataTable, column helpers, form system, and dialog management.
The CRUD System provides a complete, reusable solution for building list/detail/form pages. At its core is the useCrudViewModel hook that manages pagination, search, sorting, selection, and CRUD mutations. GenericCrudView orchestrates the DataTable, form dialogs, and confirm dialogs.
Architecture Overview
useCrudViewModel Hook
useCrudViewModel is a generic hook that encapsulates all CRUD-related state and operations. It accepts a configuration object defining data fetching, mutations, and UI options, and returns a comprehensive interface for building CRUD pages.
export interface CrudViewModelOptions {
initialPageSize?: number; // Default: 10
enabled?: boolean; // Default: true
}
/**
* Documentation for any>
*/
export function useCrudViewModel<T extends BaseEntity, TCreate = any, TUpdate = any>(
key: any[],
services: {
getAll: (params: any) => Promise<PaginatedResult<T>>;
create?: (data: TCreate) => Promise<T>;
update?: (id: string, data: TUpdate) => Promise<T>;
delete?: (id: string) => Promise<void>;
},
options: CrudViewModelOptions = {}
);interface CrudViewModelReturn<T, TCreate, TUpdate> {
// Data & Status
items: T[];
pagination: {
itemsCount: number;
pageSize: number;
page: number;
pagesCount: number;
};
loading: boolean;
error: string | null;
// Pagination State & Actions
page: number;
pageSize: number;
changePage: (newPage: number) => void;
changePageSize: (newSize: number) => void;
// Search State & Actions
searchValue: string;
searchInputRef: React.RefObject<HTMLInputElement>;
handleSearchChange: (term: string) => void;
// Selection State & Actions
selectedItems: string[];
setSelectedItems: React.Dispatch<React.SetStateAction<string[]>>;
// Modal Dialog Controllers
isCreateModalOpen: boolean;
setIsCreateModalOpen: React.Dispatch<React.SetStateAction<boolean>>;
isEditModalOpen: boolean;
setIsEditModalOpen: React.Dispatch<React.SetStateAction<boolean>>;
viewModalOpen: boolean;
setViewModalOpen: React.Dispatch<React.SetStateAction<boolean>>;
editingItem: T | null;
viewItem: T | null;
// Modal State Transitions
openEditModal: (item: T) => void;
closeEditModal: () => void;
openViewModal: (item: T) => void;
closeViewModal: () => void;
refreshItems: () => Promise<void>;
refresh: () => Promise<void>; // Alias for refreshItems
// Mutation Wrappers
createItem: (data: TCreate) => Promise<T>;
updateItem: (id: string, data: TUpdate) => Promise<T>;
deleteItem: (id: string) => Promise<void>;
// Pending States
isCreating: boolean;
isUpdating: boolean;
isDeleting: boolean;
}Zero-Flicker Query Cache Preservation
To prevent loading flashes when page or search parameters change, useGenericQuery leverages TanStack Query's placeholderData: keepPreviousData. This keeps the existing grid visible during transition, ensuring a seamless user experience.
export function useGenericQuery<T, TParams = any>(
key: any[],
fetcher: (params: TParams) => Promise<PaginatedResult<T>>,
params: TParams,
enabled: boolean = true,
options?: Omit<UseQueryOptions<PaginatedResult<T>>, "queryKey" | "queryFn">
) {
return useQuery({
queryKey: [...key, params],
queryFn: () => fetcher(params),
enabled,
placeholderData: keepPreviousData, // Keeps the existing grid visible on page/search change
...options,
});
}Optimistic Deletes & Rollback Cache
When a deletion is triggered, the UI optimistically removes the item from the list cache and cancels active queries. If the backend API call fails, the mutation automatically rolls back the cache to its previous state and displays an error toast.
const deleteMutation = useMutation({
mutationKey: [...(baseKey as unknown[]), "delete"],
mutationFn: async (id: string) => services.delete(id),
onMutate: options?.optimisticDelete
? async (id: string) => {
await queryClient.cancelQueries({ queryKey: baseKey });
const previous = queryClient.getQueryData(baseKey);
// Optimistically remove from list cache
queryClient.setQueryData(baseKey, (old: any) => {
if (!old) return old;
const items = old.items ?? old.data ?? [];
const filtered = items.filter((item: any) => item.id !== id);
return { ...old, items: filtered };
});
return { previous };
}
: undefined,
onError: (_, __, context) => {
if (context?.previous !== undefined) {
// Rollback on failure
queryClient.setQueryData(baseKey, context.previous);
}
},
onSettled: () => {
// Sync with database
queryClient.invalidateQueries({ queryKey: baseKey });
}
});GenericCrudView Component
GenericCrudView is a pre-built component that orchestrates the DataTable, FormDialog, and ConfirmDialog. It accepts a crud ViewModel instance and column definitions, handling the entire CRUD UI with minimal code.
<GenericCrudView
crud={vm.table}
columns={vm.columns}
title={t("admins.title")}
// Optional customization
createButtonLabel={t("admins.create")}
searchPlaceholder={t("admins.searchPlaceholder")}
emptyMessage={t("admins.noData")}
// Form configuration
createForm={<AdminForm mode="create" />}
editForm={<AdminForm mode="edit" item={vm.table.editingItem} />}
// Slot components
headerSlot={<FilterSection {...vm.filters} />}
beforeTableSlot={<StatisticsSection {...vm.statistics} />}
/>Column System
const column = createColumnHelper<Admin>();
// Built-in column types:
column.index("No"); // Auto-incrementing row number
column.text("firstName", t("name")); // Simple text rendering
column.date("createdAt", t("date")); // Formatted date (locale-aware)
column.date("createdAt", t("date"), { // With custom format
locale: "en-GB",
format: "dd/MM/yyyy HH:mm"
});
column.status("isActive", t("status"), { // Status badge with colors
true: { label: t("active"), color: "success" },
false: { label: t("inactive"), color: "danger" },
});
column.switch("isBlocked", t("block"), { // Toggle switch
getChecked: (row) => row.isBlocked,
onChange: (row, checked) => vm.toggleBlock(row.id, checked),
isLoading: vm.isBlockLoading,
});
column.link("email", t("email"), { // Clickable link
type: "email", // "email" | "phone" | "url"
});
column.image("avatarUrl", t("avatar"), { // Image thumbnail
width: 40, height: 40,
fallback: "/default-avatar.png",
});
column.custom("actions", t("actions"), (row) => ( // Fully custom
<ActionButtons
onEdit={() => vm.table.openEditDialog(row)}
onDelete={() => vm.table.openDeleteDialog(row)}
/>
));DataTable Features
Global Search
Debounced search across all text columns with configurable delay.
Column Sorting
Click column headers to sort. Supports multi-directional sorting (asc/desc).
Pagination
Server-side pagination with configurable page sizes (10/25/50/100).
Row Selection
Checkbox selection with select-all, bulk actions, and exclusion mode.
Responsive Layout
Responsive design with horizontal scrolling on mobile and priority columns.
RTL Support
Full RTL layout support with automatic direction switching based on language.
Form System
The form system integrates GenericForm (Zod schema-driven) with FormDialog for create/edit operations, and ConfirmDialog for destructive actions. Forms support field validation, loading states, and error display.
// GenericForm provides schema-driven form rendering
<GenericForm
schema={adminSchema}
defaultValues={editingAdmin}
onSubmit={(data) => crud.update.mutate({ id: admin.id, data })}
isLoading={crud.update.isPending}
fields={[
{ name: "firstName", label: t("firstName"), type: "text" },
{ name: "email", label: t("email"), type: "email" },
{ name: "roleId", label: t("role"), type: "select",
options: roles.map(r => ({ value: r.id, label: r.name })) },
{ name: "isActive", label: t("active"), type: "switch" },
]}
/>
// FormDialog wraps GenericForm in a dialog
<FormDialog
open={crud.isEditDialogOpen}
onClose={crud.closeDialogs}
title={t("admins.editTitle")}
>
<GenericForm ... />
</FormDialog>
// ConfirmDialog for destructive actions
<ConfirmDialog
open={crud.isDeleteDialogOpen}
onConfirm={() => crud.remove.mutate(crud.deletingItem?.id)}
onCancel={crud.closeDialogs}
title={t("admins.deleteTitle")}
message={t("admins.deleteMessage", { name: crud.deletingItem?.firstName })}
variant="danger"
isLoading={crud.remove.isPending}
/>