npm package: @kembardly/react-utils
Description: Reusable React hooks for CRUD operations with TanStack Query
Author: kembardly
Latest version: 0.1.0

# React Utils - CRUD Hooks

A collection of reusable React hooks for CRUD operations powered by TanStack Query (React Query).

## Installation

```bash
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-query
```

## Prerequisites

This package requires:

- React 18+ or React 19+
- @tanstack/react-query 5.x

Make sure to wrap your app with `QueryClientProvider`:

```tsx
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

```tsx
import { useCRUD } from "@your-scope/react-utils";
```

### Read Operation

```tsx
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

```tsx
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)

```tsx
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

```tsx
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

```typescript
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

```typescript
{
  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:

```tsx
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:

```typescript
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.