@kembardly/react-utils
v0.1.0
Published
Reusable React hooks for CRUD operations with TanStack Query
Maintainers
Readme
React Utils - CRUD Hooks
A collection of reusable React hooks for CRUD operations powered by TanStack Query (React Query).
Installation
npm install @your-scope/react-utils @tanstack/react-query
# or
pnpm add @your-scope/react-utils @tanstack/react-query
# or
yarn add @your-scope/react-utils @tanstack/react-queryPrerequisites
This package requires:
- React 18+ or React 19+
- @tanstack/react-query 5.x
Make sure to wrap your app with QueryClientProvider:
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient();
function App() {
return (
<QueryClientProvider client={queryClient}>
{/* Your app */}
</QueryClientProvider>
);
}Features
- 🎯 Type-safe CRUD operations with TypeScript
- 🔄 Built on TanStack Query - leverages caching, background updates, and more
- 🗑️ Flexible delete operations - with or without confirmation
- 📦 Lightweight - minimal dependencies
- ✅ Well-tested - comprehensive test coverage
Usage
Basic Import
import { useCRUD } from "@your-scope/react-utils";Read Operation
import { useCRUD } from "@your-scope/react-utils";
function UsersList() {
const { read } = useCRUD({
readParams: {
keys: ["users"], // Query key
fetchFn: async () => {
const response = await fetch("/api/users");
return response.json();
},
},
});
if (read.isLoading) return <div>Loading...</div>;
if (read.error) return <div>Error: {read.error.message}</div>;
return (
<ul>
{read.data?.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}Delete with Confirmation
import { useCRUD } from "@your-scope/react-utils";
function UsersListWithDelete() {
const [showConfirm, setShowConfirm] = React.useState(false);
const { delete: deleteOps } = useCRUD({
deleteParams: {
askConfirmation: true,
deleteFn: async (payload: { id: number }) => {
await fetch(`/api/users/${payload.id}`, { method: "DELETE" });
return { success: true };
},
},
});
const handleDelete = (id: number) => {
deleteOps.initiateDelete({ id });
setShowConfirm(true);
};
const handleConfirm = async () => {
await deleteOps.confirmDelete();
setShowConfirm(false);
// Optionally refetch data or update UI
};
const handleCancel = () => {
deleteOps.cancelDelete();
setShowConfirm(false);
};
return (
<div>
<button onClick={() => handleDelete(userId)}>Delete User</button>
{showConfirm && (
<div>
<p>Are you sure?</p>
<button onClick={handleConfirm}>Confirm</button>
<button onClick={handleCancel}>Cancel</button>
</div>
)}
</div>
);
}Delete Immediately (No Confirmation)
const { delete: deleteOps } = useCRUD({
deleteParams: {
deleteFn: async (payload: { id: number }) => {
await fetch(`/api/users/${payload.id}`, { method: "DELETE" });
return { success: true };
},
},
});
const handleDelete = async (id: number) => {
await deleteOps.deleteImmediately({ id });
// Handle success
};Complete CRUD Example
import { useCRUD } from "@your-scope/react-utils";
function TodoApp() {
const [todos, setTodos] = React.useState([]);
const { read, delete: deleteOps } = useCRUD({
readParams: {
keys: ["todos"],
fetchFn: async () => {
const response = await fetch("/api/todos");
const data = await response.json();
setTodos(data);
return data;
},
},
deleteParams: {
deleteFn: async (payload: { id: number }) => {
await fetch(`/api/todos/${payload.id}`, { method: "DELETE" });
setTodos((prev) => prev.filter((todo) => todo.id !== payload.id));
return { success: true };
},
},
});
const handleDelete = async (id: number) => {
await deleteOps.deleteImmediately({ id });
};
return (
<div>
{read.isLoading && <div>Loading...</div>}
{todos.map((todo) => (
<div key={todo.id}>
{todo.title}
<button onClick={() => handleDelete(todo.id)}>Delete</button>
</div>
))}
</div>
);
}API Reference
useCRUD(params)
Parameters
interface ICRUDParams<ReadResponse, DeletePayload, DeleteResponse> {
readParams?: {
keys: any[]; // TanStack Query keys
fetchFn: (params: any) => Promise<ReadResponse>;
};
deleteParams?: {
askConfirmation?: boolean;
deleteFn: (payload: DeletePayload) => Promise<DeleteResponse>;
};
}Returns
{
read: {
data: ReadResponse | undefined;
isLoading: boolean;
error: Error | null;
refetch: () => void;
// ... all TanStack Query properties
};
delete: {
// For confirmation flow
pendingDelete: DeletePayload | null;
initiateDelete: (payload: DeletePayload) => void;
confirmDelete: () => Promise<DeleteResponse | undefined>;
cancelDelete: () => void;
// For immediate deletion
deleteImmediately: (payload: DeletePayload) => Promise<DeleteResponse | undefined>;
// TanStack Query mutation properties
isPending: boolean;
error: Error | null;
// ... other mutation properties
};
}Error Handling
Errors from your fetchFn and deleteFn are passed through directly, allowing you to handle them as needed:
import { useCRUD } from "@your-scope/react-utils";
const { read, delete: deleteOps } = useCRUD({
readParams: {
keys: ["users"],
fetchFn: async () => {
const response = await fetch("/api/users");
if (!response.ok) throw new Error("Failed to fetch");
return response.json();
},
},
});
// Handle read errors
if (read.error) {
console.error("Read error:", read.error.message);
}
// Handle delete errors
try {
await deleteOps.confirmDelete();
} catch (error) {
console.error("Delete error:", error);
}TypeScript Support
Full TypeScript support with generics:
interface User {
id: number;
name: string;
}
interface DeleteResponse {
success: boolean;
message?: string;
}
const { read, delete: deleteOps } = useCRUD<
User[], // ReadResponse
{ id: number }, // DeletePayload
DeleteResponse // DeleteResponse
>({
readParams: {
keys: ["users"],
fetchFn: async () => {
// TypeScript knows this returns User[]
return await fetchUsers();
},
},
deleteParams: {
deleteFn: async (payload) => {
// payload is typed as { id: number }
return await deleteUser(payload.id);
},
},
});License
MIT
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
