diff --git a/examples/useCreateMutation-example.tsx b/examples/useCreateMutation-example.tsx
index a9523c1..142bcbe 100644
--- a/examples/useCreateMutation-example.tsx
+++ b/examples/useCreateMutation-example.tsx
@@ -13,37 +13,38 @@ function App() {
}
function CreateMutationExample() {
- const { mutate: createPost, isPending, isSuccess, error } = useCreateMutation('posts');
const { data: posts } = useCollection('posts', { perPage: 10 });
+ const { mutateAsync: createPost, isPending, isSuccess, isError, error } = useCreateMutation('posts');
const [title, setTitle] = useState('');
const [content, setContent] = useState('');
const [status, setStatus] = useState<'draft' | 'published'>('draft');
const handleCreate = async (e: FormEvent) => {
e.preventDefault();
+ if (!title.trim() || !content.trim()) return;
+
try {
const newPost = await createPost({
- title,
- content,
+ title: title.trim(),
+ content: content.trim(),
status,
});
console.log('Post created:', newPost);
setTitle('');
setContent('');
+ setStatus('draft');
} catch (err) {
console.error('Failed to create post:', err);
}
};
- if (error) return
Error: {error}
;
-
return (
-
Create Post
+
Create New Post
diff --git a/examples/useDeleteMutation-example.tsx b/examples/useDeleteMutation-example.tsx
index 561c708..d879bfe 100644
--- a/examples/useDeleteMutation-example.tsx
+++ b/examples/useDeleteMutation-example.tsx
@@ -13,28 +13,34 @@ function App() {
}
function DeleteMutationExample() {
- const { mutate: deletePost, isPending, isSuccess, error } = useDeleteMutation('posts');
const { data: posts } = useCollection('posts', { perPage: 10 });
const [deletingId, setDeletingId] = useState
(null);
+ const { mutateAsync: deletePost, isPending, isSuccess, isError, error } = useDeleteMutation('posts', deletingId);
- const handleDelete = async (id: string) => {
- if (window.confirm('Are you sure you want to delete this post?')) {
- setDeletingId(id);
- try {
- const success = await deletePost(id);
- if (success) {
- console.log('Post deleted successfully');
- }
- } catch (err) {
- console.error('Failed to delete post:', err);
- } finally {
- setDeletingId(null);
- }
+ const handleDelete = async (postId: string) => {
+ if (isPending) {
+ return;
+ }
+
+ setDeletingId(postId);
+ try {
+ await deletePost();
+ console.log('Post deleted successfully');
+ } catch (err) {
+ console.error('Failed to delete post:', err);
+ } finally {
+ setDeletingId(null);
+ }
+ };
+
+ const confirmDelete = (post: RecordModel) => {
+ if (window.confirm(`Are you sure you want to delete "${post.title}"?`)) {
+ handleDelete(post.id);
}
};
- if (error) return Error: {error}
;
+ if (isError) return Error: {error}
;
return (
@@ -53,18 +59,17 @@ function DeleteMutationExample() {
diff --git a/examples/useUpdateMutation-example.tsx b/examples/useUpdateMutation-example.tsx
index a655f5b..267b599 100644
--- a/examples/useUpdateMutation-example.tsx
+++ b/examples/useUpdateMutation-example.tsx
@@ -13,10 +13,10 @@ function App() {
}
function UpdateMutationExample() {
- const { mutate: updatePost, isPending, isSuccess, error } = useUpdateMutation('posts');
const { data: posts } = useCollection('posts', { perPage: 10 });
const [editingId, setEditingId] = useState(null);
+ const { mutateAsync: updatePost, isPending, isSuccess, isError, error } = useUpdateMutation('posts', editingId);
const [editTitle, setEditTitle] = useState('');
const [editContent, setEditContent] = useState('');
const [editStatus, setEditStatus] = useState<'draft' | 'published'>('draft');
@@ -26,7 +26,7 @@ function UpdateMutationExample() {
if (!editingId) return;
try {
- const updatedPost = await updatePost(editingId, {
+ const updatedPost = await updatePost({
title: editTitle,
content: editContent,
status: editStatus,
@@ -53,8 +53,6 @@ function UpdateMutationExample() {
setEditContent('');
};
- if (error) return Error: {error}
;
-
return (
Update Posts
@@ -81,6 +79,7 @@ function UpdateMutationExample() {
Cancel
{isSuccess &&
Post updated successfully!
}
+ {isError && error &&
Error: {error}
}
)}
diff --git a/src/hooks/useCreateMutation.ts b/src/hooks/useCreateMutation.ts
index 0b40285..66f6bc9 100644
--- a/src/hooks/useCreateMutation.ts
+++ b/src/hooks/useCreateMutation.ts
@@ -12,12 +12,14 @@ import { usePocketBase } from './usePocketBase';
*
* @example
* ```tsx
- * const { mutate, isPending, isSuccess, error } = useCreateMutation
('posts');
+ * const { mutateAsync, isPending, isSuccess, isError, error } = useCreateMutation('posts');
*
* const handleCreate = async () => {
- * const newPost = await mutate({ title: 'Hello', content: 'World' });
- * if (newPost) {
+ * try {
+ * const newPost = await mutateAsync({ title: 'Hello', content: 'World' });
* console.log('Created:', newPost);
+ * } catch (err) {
+ * console.error('Failed to create post:', err);
* }
* };
* ```
@@ -28,16 +30,17 @@ export function useCreateMutation(collectionName: st
const [isPending, setIsPending] = useState(false);
const [error, setError] = useState(null);
- const mutate = useCallback(
- async (bodyParams: Partial, options?: RecordOptions): Promise => {
+ const mutateAsync = useCallback(
+ async (bodyParams: Partial, options?: RecordOptions): Promise => {
try {
setIsPending(true);
setError(null);
- const record = await recordService.create(bodyParams, options);
+ const record = options ? await recordService.create(bodyParams, options) : await recordService.create(bodyParams);
return record as Record;
} catch (err) {
- setError(err instanceof Error ? err.message : 'Error creating record');
- return null;
+ const errorMessage = err instanceof Error ? err.message : 'Error creating record';
+ setError(errorMessage);
+ throw new Error(errorMessage);
} finally {
setIsPending(false);
}
@@ -45,13 +48,24 @@ export function useCreateMutation(collectionName: st
[recordService],
);
+ const mutate = useCallback(
+ (bodyParams: Partial, options?: RecordOptions): void => {
+ mutateAsync(bodyParams, options).catch(() => {
+ // Error is already handled in mutateAsync
+ });
+ },
+ [mutateAsync],
+ );
+
return useMemo(
(): UseCreateMutationResult => ({
mutate,
+ mutateAsync,
isPending,
+ isError: !!error,
error,
isSuccess: !isPending && !error,
}),
- [mutate, isPending, error],
+ [mutate, mutateAsync, isPending, error],
);
}
diff --git a/src/hooks/useDeleteMutation.ts b/src/hooks/useDeleteMutation.ts
index 4279faa..8397cf1 100644
--- a/src/hooks/useDeleteMutation.ts
+++ b/src/hooks/useDeleteMutation.ts
@@ -1,4 +1,4 @@
-import type { CommonOptions } from 'pocketbase';
+import type { CommonOptions, RecordModel } from 'pocketbase';
import { useCallback, useMemo, useState } from 'react';
import type { UseDeleteMutationResult } from '../types';
import { usePocketBase } from './usePocketBase';
@@ -7,6 +7,7 @@ import { usePocketBase } from './usePocketBase';
* Hook for deleting records from a PocketBase collection.
*
* @param collectionName - The name of the PocketBase collection
+ * @param id - The ID of the record to delete
* @returns An object containing the mutate function and mutation state
*
* @example
@@ -21,36 +22,51 @@ import { usePocketBase } from './usePocketBase';
* };
* ```
*/
-export function useDeleteMutation(collectionName: string): UseDeleteMutationResult {
+export function useDeleteMutation(collectionName: string, id: Record['id'] | null): UseDeleteMutationResult {
const pb = usePocketBase();
const recordService = useMemo(() => pb.collection(collectionName), [pb, collectionName]);
const [isPending, setIsPending] = useState(false);
const [error, setError] = useState(null);
- const mutate = useCallback(
- async (id: string, options?: CommonOptions): Promise => {
+ const mutateAsync = useCallback(
+ async (options?: CommonOptions): Promise => {
+ if (!id) {
+ throw new Error('ID is required');
+ }
+
try {
setIsPending(true);
setError(null);
await recordService.delete(id, options);
- return true;
} catch (err) {
- setError(err instanceof Error ? err.message : 'Error deleting record');
- return false;
+ const errorMessage = err instanceof Error ? err.message : 'Error deleting record';
+ setError(errorMessage);
+ throw new Error(errorMessage);
} finally {
setIsPending(false);
}
},
- [recordService],
+ [recordService, id],
+ );
+
+ const mutate = useCallback(
+ (options?: CommonOptions): void => {
+ mutateAsync(options).catch(() => {
+ // Error is already handled in mutateAsync
+ });
+ },
+ [mutateAsync],
);
return useMemo(
(): UseDeleteMutationResult => ({
mutate,
+ mutateAsync,
isPending,
+ isError: !!error,
error,
isSuccess: !isPending && !error,
}),
- [mutate, isPending, error],
+ [mutate, mutateAsync, isPending, error],
);
}
diff --git a/src/hooks/useUpdateMutation.ts b/src/hooks/useUpdateMutation.ts
index 55f32fd..a179eaa 100644
--- a/src/hooks/useUpdateMutation.ts
+++ b/src/hooks/useUpdateMutation.ts
@@ -8,6 +8,7 @@ import { usePocketBase } from './usePocketBase';
*
* @template Record - The record type extending RecordModel
* @param collectionName - The name of the PocketBase collection
+ * @param id - The ID of the record to update
* @returns An object containing the mutate function and mutation state
*
* @example
@@ -22,36 +23,52 @@ import { usePocketBase } from './usePocketBase';
* };
* ```
*/
-export function useUpdateMutation(collectionName: string): UseUpdateMutationResult {
+export function useUpdateMutation(collectionName: string, id: Record['id'] | null): UseUpdateMutationResult {
const pb = usePocketBase();
const recordService = useMemo(() => pb.collection(collectionName), [pb, collectionName]);
const [isPending, setIsPending] = useState(false);
const [error, setError] = useState(null);
- const mutate = useCallback(
- async (id: string, bodyParams: Partial, options?: RecordOptions): Promise => {
+ const mutateAsync = useCallback(
+ async (bodyParams: Partial, options?: RecordOptions): Promise => {
+ if (!id) {
+ throw new Error('ID is required');
+ }
+
try {
setIsPending(true);
setError(null);
const record = options ? await recordService.update(id, bodyParams, options) : await recordService.update(id, bodyParams);
return record as Record;
} catch (err) {
- setError(err instanceof Error ? err.message : 'Error updating record');
- return null;
+ const errorMessage = err instanceof Error ? err.message : 'Error updating record';
+ setError(errorMessage);
+ throw new Error(errorMessage);
} finally {
setIsPending(false);
}
},
- [recordService],
+ [recordService, id],
+ );
+
+ const mutate = useCallback(
+ (bodyParams: Partial, options?: RecordOptions): void => {
+ mutateAsync(bodyParams, options).catch(() => {
+ // Error is already handled in mutateAsync
+ });
+ },
+ [mutateAsync],
);
return useMemo(
(): UseUpdateMutationResult => ({
mutate,
+ mutateAsync,
isPending,
+ isError: !!error,
error,
isSuccess: !isPending && !error,
}),
- [mutate, isPending, error],
+ [mutate, mutateAsync, isPending, error],
);
}
diff --git a/src/types/index.ts b/src/types/index.ts
index b25ee23..86aa6c2 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -3,8 +3,8 @@ export * from './query-result.type';
export * from './record-transformer.type';
export * from './useAuth.type';
export * from './useCollection.type';
-export * from './useCommon.type';
export * from './useCreateMutation.type';
export * from './useDeleteMutation.type';
+export * from './useQueryCommon.type';
export * from './useRecord.type';
export * from './useUpdateMutation.type';
diff --git a/src/types/useCollection.type.ts b/src/types/useCollection.type.ts
index bce43c4..964b6bf 100644
--- a/src/types/useCollection.type.ts
+++ b/src/types/useCollection.type.ts
@@ -1,13 +1,13 @@
import type { RecordModel } from 'pocketbase';
import type { QueryResult } from './query-result.type';
-import type { UseCommonOptions } from './useCommon.type';
+import type { UseQueryCommonOptions } from './useQueryCommon.type';
/**
* Options for configuring the useCollection hook.
*
* @template T - The record type extending RecordModel
*/
-export interface UseCollectionOptions extends UseCommonOptions {
+export interface UseCollectionOptions extends UseQueryCommonOptions {
/**
* PocketBase filter query (e.g., 'published = true')
*/
diff --git a/src/types/useCreateMutation.type.ts b/src/types/useCreateMutation.type.ts
index e9542af..3e6a8fc 100644
--- a/src/types/useCreateMutation.type.ts
+++ b/src/types/useCreateMutation.type.ts
@@ -1,31 +1,25 @@
import type { RecordModel, RecordOptions } from 'pocketbase';
+import type { UseMutationCommonOptions } from './useMutationCommon.type';
/**
* Result type returned by useCreateMutation hook.
*
* @template Record - The record type extending RecordModel
*/
-export interface UseCreateMutationResult {
+export interface UseCreateMutationResult extends UseMutationCommonOptions {
/**
- * Function to create a new record. Returns the created record on success, null on error.
+ * Function to create a new record synchronously. Triggers the mutation but doesn't wait for completion.
*
* @param bodyParams - Partial record data to create
* @param options - Optional PocketBase record options (expand, fields, etc.)
*/
- mutate: (bodyParams: Partial, options?: RecordOptions) => Promise;
+ mutate: (bodyParams: Partial, options?: RecordOptions) => void;
/**
- * True when the mutation is in progress
- */
- isPending: boolean;
-
- /**
- * True when the mutation completed successfully (not pending and no error)
- */
- isSuccess: boolean;
-
- /**
- * Error message if the mutation failed, null otherwise
+ * Function to create a new record asynchronously. Returns a promise that resolves with the created record.
+ *
+ * @param bodyParams - Partial record data to create
+ * @param options - Optional PocketBase record options (expand, fields, etc.)
*/
- error: string | null;
+ mutateAsync: (bodyParams: Partial, options?: RecordOptions) => Promise;
}
diff --git a/src/types/useDeleteMutation.type.ts b/src/types/useDeleteMutation.type.ts
index b10b7d1..a8f14eb 100644
--- a/src/types/useDeleteMutation.type.ts
+++ b/src/types/useDeleteMutation.type.ts
@@ -1,29 +1,21 @@
import type { CommonOptions } from 'pocketbase';
+import type { UseMutationCommonOptions } from './useMutationCommon.type';
/**
* Result type returned by useDeleteMutation hook.
*/
-export interface UseDeleteMutationResult {
+export interface UseDeleteMutationResult extends UseMutationCommonOptions {
/**
- * Function to delete a record. Returns true on success, false on error.
+ * Function to delete a record synchronously. Triggers the mutation but doesn't wait for completion.
*
- * @param id - The ID of the record to delete
* @param options - Optional PocketBase common options (headers, fetch, etc.)
*/
- mutate: (id: string, options?: CommonOptions) => Promise;
+ mutate: (options?: CommonOptions) => void;
/**
- * True when the mutation is in progress
- */
- isPending: boolean;
-
- /**
- * True when the mutation completed successfully (not pending and no error)
- */
- isSuccess: boolean;
-
- /**
- * Error message if the mutation failed, null otherwise
+ * Function to delete a record asynchronously. Returns a promise that resolves when deletion is complete.
+ *
+ * @param options - Optional PocketBase common options (headers, fetch, etc.)
*/
- error: string | null;
+ mutateAsync: (options?: CommonOptions) => Promise;
}
diff --git a/src/types/useMutationCommon.type.ts b/src/types/useMutationCommon.type.ts
new file mode 100644
index 0000000..002b301
--- /dev/null
+++ b/src/types/useMutationCommon.type.ts
@@ -0,0 +1,21 @@
+export interface UseMutationCommonOptions {
+ /**
+ * True when the mutation is in progress
+ */
+ isPending: boolean;
+
+ /**
+ * True when the mutation completed successfully (not pending and no error)
+ */
+ isSuccess: boolean;
+
+ /**
+ * True when the mutation failed
+ */
+ isError: boolean;
+
+ /**
+ * Error message if the mutation failed, null otherwise
+ */
+ error: string | null;
+}
diff --git a/src/types/useCommon.type.ts b/src/types/useQueryCommon.type.ts
similarity index 85%
rename from src/types/useCommon.type.ts
rename to src/types/useQueryCommon.type.ts
index 98ef26e..e70e554 100644
--- a/src/types/useCommon.type.ts
+++ b/src/types/useQueryCommon.type.ts
@@ -1,7 +1,7 @@
import type { RecordModel } from 'pocketbase';
import type { RecordTransformer } from './record-transformer.type';
-export interface UseCommonOptions {
+export interface UseQueryCommonOptions {
/**
* Expand related records (e.g., 'author,comments')
*/
diff --git a/src/types/useRecord.type.ts b/src/types/useRecord.type.ts
index b116894..d7fff93 100644
--- a/src/types/useRecord.type.ts
+++ b/src/types/useRecord.type.ts
@@ -1,13 +1,13 @@
import type { RecordModel } from 'pocketbase';
import type { QueryResult } from './query-result.type';
-import type { UseCommonOptions } from './useCommon.type';
+import type { UseQueryCommonOptions } from './useQueryCommon.type';
/**
* Options for configuring the useRecord hook.
*
* @template T - The record type extending RecordModel
*/
-export interface UseRecordOptions extends UseCommonOptions {
+export interface UseRecordOptions extends UseQueryCommonOptions {
/**
* Default value to use before data is loaded
*/
diff --git a/src/types/useUpdateMutation.type.ts b/src/types/useUpdateMutation.type.ts
index 335714a..6d26e0e 100644
--- a/src/types/useUpdateMutation.type.ts
+++ b/src/types/useUpdateMutation.type.ts
@@ -1,32 +1,25 @@
import type { RecordModel, RecordOptions } from 'pocketbase';
+import type { UseMutationCommonOptions } from './useMutationCommon.type';
/**
* Result type returned by useUpdateMutation hook.
*
* @template Record - The record type extending RecordModel
*/
-export interface UseUpdateMutationResult {
+export interface UseUpdateMutationResult extends UseMutationCommonOptions {
/**
- * Function to update an existing record. Returns the updated record on success, null on error.
+ * Function to update an existing record synchronously. Triggers the mutation but doesn't wait for completion.
*
- * @param id - The ID of the record to update
* @param bodyParams - Partial record data to update
* @param options - Optional PocketBase record options (expand, fields, etc.)
*/
- mutate: (id: string, bodyParams: Partial, options?: RecordOptions) => Promise;
+ mutate: (bodyParams: Partial, options?: RecordOptions) => void;
/**
- * True when the mutation is in progress
- */
- isPending: boolean;
-
- /**
- * True when the mutation completed successfully (not pending and no error)
- */
- isSuccess: boolean;
-
- /**
- * Error message if the mutation failed, null otherwise
+ * Function to update an existing record asynchronously. Returns a promise that resolves with the updated record.
+ *
+ * @param bodyParams - Partial record data to update
+ * @param options - Optional PocketBase record options (expand, fields, etc.)
*/
- error: string | null;
+ mutateAsync: (bodyParams: Partial, options?: RecordOptions) => Promise;
}
diff --git a/tests/hooks/useCollection.test.tsx b/tests/hooks/useCollection.test.tsx
index 9be06b0..381563c 100644
--- a/tests/hooks/useCollection.test.tsx
+++ b/tests/hooks/useCollection.test.tsx
@@ -738,7 +738,7 @@ describe('useCollection', () => {
return Promise.resolve(() => {});
});
- const faultyTransformer = (record: any) => {
+ const faultyTransformer = (_record: any) => {
throw new Error('Transformer error');
};
diff --git a/tests/hooks/useCreateMutation.test.tsx b/tests/hooks/useCreateMutation.test.tsx
index 9118088..97e0a58 100644
--- a/tests/hooks/useCreateMutation.test.tsx
+++ b/tests/hooks/useCreateMutation.test.tsx
@@ -22,113 +22,183 @@ describe('useCreateMutation', () => {
expect(result.current.isPending).toBe(false);
expect(result.current.error).toBe(null);
expect(result.current.isSuccess).toBe(true);
+ expect(result.current.isError).toBe(false);
expect(typeof result.current.mutate).toBe('function');
+ expect(typeof result.current.mutateAsync).toBe('function');
});
- it('should handle create mutation', async () => {
- const mockData = { id: '1', title: 'New Item' };
- mockCreate.mockResolvedValue(mockData);
+ it('should throw error when used outside provider', () => {
+ expect(() => {
+ renderHook(() => useCreateMutation('test'));
+ }).toThrow('usePocketBase must be used within a PocketBaseProvider');
+ });
- const wrapper = createWrapper(mockPocketBase);
+ describe('mutateAsync', () => {
+ it('should handle successful create', async () => {
+ const mockData = { id: '1', title: 'New Item' };
+ mockCreate.mockResolvedValue(mockData);
- const { result } = renderHook(() => useCreateMutation('test'), { wrapper });
+ const wrapper = createWrapper(mockPocketBase);
- const mutationResult = await result.current.mutate({
- title: 'New Item',
- });
+ const { result } = renderHook(() => useCreateMutation('test'), { wrapper });
- return waitFor(() => {
- expect(mockCreate).toHaveBeenCalledWith(
- {
- title: 'New Item',
- },
- undefined,
- );
+ const mutationResult = await result.current.mutateAsync({
+ title: 'New Item',
+ });
+
+ expect(mockCreate).toHaveBeenCalledWith({ title: 'New Item' });
expect(mutationResult).toEqual(mockData);
expect(result.current.isPending).toBe(false);
expect(result.current.isSuccess).toBe(true);
});
- });
- it('should handle create mutation with options', async () => {
- const mockData = { id: '1', title: 'New Item' };
- mockCreate.mockResolvedValue(mockData);
+ it('should handle create with options', async () => {
+ const mockData = { id: '1', title: 'New Item' };
+ mockCreate.mockResolvedValue(mockData);
- const wrapper = createWrapper(mockPocketBase);
+ const wrapper = createWrapper(mockPocketBase);
- const { result } = renderHook(() => useCreateMutation('test'), { wrapper });
+ const { result } = renderHook(() => useCreateMutation('test'), { wrapper });
- const options = { expand: 'relation' };
+ const options = { expand: 'relation' };
+ await result.current.mutateAsync({ title: 'New Item' }, options);
- await result.current.mutate({ title: 'New Item' }, options);
-
- return waitFor(() => {
expect(mockCreate).toHaveBeenCalledWith({ title: 'New Item' }, options);
});
- });
- it('should handle mutation error', async () => {
- const mockError = new Error('Create failed');
- mockCreate.mockRejectedValue(mockError);
+ it('should handle mutation error', async () => {
+ const mockError = new Error('Create failed');
+ mockCreate.mockRejectedValue(mockError);
- const wrapper = createWrapper(mockPocketBase);
+ const wrapper = createWrapper(mockPocketBase);
- const { result } = renderHook(() => useCreateMutation('test'), { wrapper });
+ const { result } = renderHook(() => useCreateMutation('test'), { wrapper });
- await result.current.mutate({ title: 'Test' });
+ await expect(result.current.mutateAsync({ title: 'Test' })).rejects.toThrow('Create failed');
- return waitFor(() => {
- expect(result.current.error).toEqual('Create failed');
- expect(result.current.isPending).toBe(false);
- expect(result.current.isSuccess).toBe(false);
+ await waitFor(() => {
+ expect(result.current.error).toEqual('Create failed');
+ expect(result.current.isPending).toBe(false);
+ expect(result.current.isSuccess).toBe(false);
+ expect(result.current.isError).toBe(true);
+ });
});
- });
- it('should handle non-Error exceptions', async () => {
- mockCreate.mockRejectedValue('String error');
+ it('should handle non-Error exceptions', async () => {
+ mockCreate.mockRejectedValue('String error');
- const wrapper = createWrapper(mockPocketBase);
+ const wrapper = createWrapper(mockPocketBase);
- const { result } = renderHook(() => useCreateMutation('test'), { wrapper });
+ const { result } = renderHook(() => useCreateMutation('test'), { wrapper });
- await result.current.mutate({ title: 'Test' });
+ await expect(result.current.mutateAsync({ title: 'Test' })).rejects.toThrow('Error creating record');
- return waitFor(() => {
- expect(result.current.error).toBe('Error creating record');
- expect(result.current.isPending).toBe(false);
- expect(result.current.isSuccess).toBe(false);
+ await waitFor(() => {
+ expect(result.current.error).toBe('Error creating record');
+ expect(result.current.isPending).toBe(false);
+ expect(result.current.isSuccess).toBe(false);
+ expect(result.current.isError).toBe(true);
+ });
+ });
+
+ it('should set isPending to true during mutation', async () => {
+ let resolveCreate: (value: unknown) => void = () => {};
+ const createPromise = new Promise((resolve) => {
+ resolveCreate = resolve;
+ });
+ mockCreate.mockReturnValue(createPromise);
+
+ const wrapper = createWrapper(mockPocketBase);
+
+ const { result } = renderHook(() => useCreateMutation('test'), { wrapper });
+
+ const mutationPromise = result.current.mutateAsync({ title: 'Test' });
+
+ await waitFor(() => {
+ expect(result.current.isPending).toBe(true);
+ expect(result.current.isSuccess).toBe(false);
+ });
+
+ resolveCreate({ id: '1', title: 'Test' });
+ await mutationPromise;
+
+ await waitFor(() => {
+ expect(result.current.isPending).toBe(false);
+ expect(result.current.isSuccess).toBe(true);
+ });
});
});
- it('should set isPending to true during mutation', async () => {
- let resolveCreate: (value: unknown) => void;
- const createPromise = new Promise((resolve) => {
- resolveCreate = resolve;
+ describe('mutate', () => {
+ it('should handle successful create', async () => {
+ const mockData = { id: '1', title: 'New Item' };
+ mockCreate.mockResolvedValue(mockData);
+
+ const wrapper = createWrapper(mockPocketBase);
+
+ const { result } = renderHook(() => useCreateMutation('test'), { wrapper });
+
+ const mutateResult = result.current.mutate({ title: 'New Item' });
+ expect(mutateResult).toBeUndefined();
+
+ await waitFor(() => {
+ expect(mockCreate).toHaveBeenCalledWith({ title: 'New Item' });
+ expect(result.current.isPending).toBe(false);
+ expect(result.current.isSuccess).toBe(true);
+ });
});
- mockCreate.mockReturnValue(createPromise);
- const wrapper = createWrapper(mockPocketBase);
+ it('should handle create with options', async () => {
+ const mockData = { id: '1', title: 'New Item' };
+ mockCreate.mockResolvedValue(mockData);
- const { result } = renderHook(() => useCreateMutation('test'), { wrapper });
+ const wrapper = createWrapper(mockPocketBase);
- waitFor(() => {
- result.current.mutate({ title: 'Test' });
- expect(result.current.isPending).toBe(true);
- expect(result.current.isSuccess).toBe(false);
- resolveCreate({ id: '1', title: 'Test' });
+ const { result } = renderHook(() => useCreateMutation('test'), { wrapper });
+
+ const options = { expand: 'relation' };
+ result.current.mutate({ title: 'New Item' }, options);
+
+ await waitFor(() => {
+ expect(mockCreate).toHaveBeenCalledWith({ title: 'New Item' }, options);
+ expect(result.current.isPending).toBe(false);
+ expect(result.current.isSuccess).toBe(true);
+ });
});
- await createPromise;
+ it('should handle mutation error', async () => {
+ const mockError = new Error('Create failed');
+ mockCreate.mockRejectedValue(mockError);
- return waitFor(() => {
- expect(result.current.isPending).toBe(false);
- expect(result.current.isSuccess).toBe(true);
+ const wrapper = createWrapper(mockPocketBase);
+
+ const { result } = renderHook(() => useCreateMutation('test'), { wrapper });
+
+ result.current.mutate({ title: 'Test' });
+
+ await waitFor(() => {
+ expect(result.current.error).toEqual('Create failed');
+ expect(result.current.isPending).toBe(false);
+ expect(result.current.isSuccess).toBe(false);
+ expect(result.current.isError).toBe(true);
+ });
});
- });
- it('should throw error when used outside provider', () => {
- expect(() => {
- renderHook(() => useCreateMutation('test'));
- }).toThrow('usePocketBase must be used within a PocketBaseProvider');
+ it('should handle non-Error exceptions', async () => {
+ mockCreate.mockRejectedValue('String error');
+
+ const wrapper = createWrapper(mockPocketBase);
+
+ const { result } = renderHook(() => useCreateMutation('test'), { wrapper });
+
+ result.current.mutate({ title: 'Test' });
+
+ await waitFor(() => {
+ expect(result.current.error).toBe('Error creating record');
+ expect(result.current.isPending).toBe(false);
+ expect(result.current.isSuccess).toBe(false);
+ expect(result.current.isError).toBe(true);
+ });
+ });
});
});
diff --git a/tests/hooks/useDeleteMutation.test.tsx b/tests/hooks/useDeleteMutation.test.tsx
index b03c6e3..7bdf99c 100644
--- a/tests/hooks/useDeleteMutation.test.tsx
+++ b/tests/hooks/useDeleteMutation.test.tsx
@@ -17,111 +17,189 @@ describe('useDeleteMutation', () => {
it('should return initial state', () => {
const wrapper = createWrapper(mockPocketBase);
- const { result } = renderHook(() => useDeleteMutation('test'), { wrapper });
+ const { result } = renderHook(() => useDeleteMutation('test', '1'), { wrapper });
expect(result.current.isPending).toBe(false);
expect(result.current.error).toBe(null);
expect(result.current.isSuccess).toBe(true);
+ expect(result.current.isError).toBe(false);
expect(typeof result.current.mutate).toBe('function');
+ expect(typeof result.current.mutateAsync).toBe('function');
});
- it('should handle delete mutation', async () => {
- mockDelete.mockResolvedValue(true);
+ it('should throw error when used outside provider', () => {
+ expect(() => {
+ renderHook(() => useDeleteMutation('test', '1'));
+ }).toThrow('usePocketBase must be used within a PocketBaseProvider');
+ });
+ it('should handle null id', async () => {
const wrapper = createWrapper(mockPocketBase);
- const { result } = renderHook(() => useDeleteMutation('test'), { wrapper });
+ const { result } = renderHook(() => useDeleteMutation('test', null), { wrapper });
+
+ await expect(result.current.mutateAsync()).rejects.toThrow('ID is required');
+ });
+
+ describe('mutateAsync', () => {
+ it('should handle successful delete', async () => {
+ mockDelete.mockResolvedValue(true);
+
+ const wrapper = createWrapper(mockPocketBase);
+
+ const { result } = renderHook(() => useDeleteMutation('test', '1'), { wrapper });
- const mutationResult = await result.current.mutate('1');
+ await result.current.mutateAsync();
- return waitFor(() => {
expect(mockDelete).toHaveBeenCalledWith('1', undefined);
- expect(mutationResult).toBe(true);
expect(result.current.isPending).toBe(false);
expect(result.current.isSuccess).toBe(true);
});
- });
- it('should handle delete mutation with options', async () => {
- mockDelete.mockResolvedValue(true);
+ it('should handle delete with options', async () => {
+ mockDelete.mockResolvedValue(true);
- const wrapper = createWrapper(mockPocketBase);
+ const wrapper = createWrapper(mockPocketBase);
- const { result } = renderHook(() => useDeleteMutation('test'), { wrapper });
+ const { result } = renderHook(() => useDeleteMutation('test', '1'), { wrapper });
- const options = { headers: { 'X-Custom': 'value' } };
+ const options = { headers: { 'X-Custom': 'value' } };
- await result.current.mutate('1', options);
- return waitFor(() => {
+ await result.current.mutateAsync(options);
expect(mockDelete).toHaveBeenCalledWith('1', options);
});
- });
- it('should handle mutation error', async () => {
- const mockError = new Error('Delete failed');
- mockDelete.mockRejectedValue(mockError);
+ it('should handle mutation error', async () => {
+ const mockError = new Error('Delete failed');
+ mockDelete.mockRejectedValue(mockError);
- const wrapper = createWrapper(mockPocketBase);
+ const wrapper = createWrapper(mockPocketBase);
- const { result } = renderHook(() => useDeleteMutation('test'), { wrapper });
+ const { result } = renderHook(() => useDeleteMutation('test', '1'), { wrapper });
- await result.current.mutate('1');
+ await expect(result.current.mutateAsync()).rejects.toThrow('Delete failed');
- return waitFor(() => {
- expect(result.current.error).toEqual('Delete failed');
- expect(result.current.isPending).toBe(false);
- expect(result.current.isSuccess).toBe(false);
+ await waitFor(() => {
+ expect(result.current.error).toEqual('Delete failed');
+ expect(result.current.isPending).toBe(false);
+ expect(result.current.isSuccess).toBe(false);
+ expect(result.current.isError).toBe(true);
+ });
});
- });
- it('should handle non-Error exceptions', async () => {
- mockDelete.mockRejectedValue('String error');
+ it('should handle non-Error exceptions', async () => {
+ mockDelete.mockRejectedValue('String error');
- const wrapper = createWrapper(mockPocketBase);
+ const wrapper = createWrapper(mockPocketBase);
- const { result } = renderHook(() => useDeleteMutation('test'), { wrapper });
+ const { result } = renderHook(() => useDeleteMutation('test', '1'), { wrapper });
- await result.current.mutate('1');
+ await expect(result.current.mutateAsync()).rejects.toThrow('Error deleting record');
- return waitFor(() => {
- expect(result.current.error).toBe('Error deleting record');
- expect(result.current.isPending).toBe(false);
- expect(result.current.isSuccess).toBe(false);
+ await waitFor(() => {
+ expect(result.current.error).toBe('Error deleting record');
+ expect(result.current.isPending).toBe(false);
+ expect(result.current.isSuccess).toBe(false);
+ expect(result.current.isError).toBe(true);
+ });
});
- });
- it('should set isPending to true during mutation', async () => {
- let resolveDelete: (value: unknown) => void;
- const deletePromise = new Promise((resolve) => {
- resolveDelete = resolve;
+ it('should set isPending to true during mutation', async () => {
+ let resolveDelete: (value: unknown) => void = () => {};
+ const deletePromise = new Promise((resolve) => {
+ resolveDelete = resolve;
+ });
+ mockDelete.mockReturnValue(deletePromise);
+
+ const wrapper = createWrapper(mockPocketBase);
+
+ const { result } = renderHook(() => useDeleteMutation('test', '1'), { wrapper });
+
+ const mutationPromise = result.current.mutateAsync();
+
+ await waitFor(() => {
+ expect(result.current.isPending).toBe(true);
+ expect(result.current.isSuccess).toBe(false);
+ });
+
+ resolveDelete(true);
+ await mutationPromise;
+
+ await waitFor(() => {
+ expect(result.current.isPending).toBe(false);
+ expect(result.current.isSuccess).toBe(true);
+ });
});
- mockDelete.mockReturnValue(deletePromise);
+ });
- const wrapper = createWrapper(mockPocketBase);
+ describe('mutate', () => {
+ it('should handle successful delete', async () => {
+ mockDelete.mockResolvedValue(true);
- const { result } = renderHook(() => useDeleteMutation('test'), { wrapper });
+ const wrapper = createWrapper(mockPocketBase);
- waitFor(() => {
- result.current.mutate('1');
- expect(result.current.isPending).toBe(true);
- expect(result.current.isSuccess).toBe(false);
+ const { result } = renderHook(() => useDeleteMutation('test', '1'), { wrapper });
+
+ const mutateResult = result.current.mutate();
+ expect(mutateResult).toBeUndefined();
+
+ await waitFor(() => {
+ expect(mockDelete).toHaveBeenCalledWith('1', undefined);
+ expect(result.current.isPending).toBe(false);
+ expect(result.current.isSuccess).toBe(true);
+ });
});
- await waitFor(() => {
- resolveDelete(true);
+ it('should handle delete with options', async () => {
+ mockDelete.mockResolvedValue(true);
+
+ const wrapper = createWrapper(mockPocketBase);
+
+ const { result } = renderHook(() => useDeleteMutation('test', '1'), { wrapper });
+
+ const options = { headers: { 'X-Custom': 'value' } };
+ result.current.mutate(options);
+
+ await waitFor(() => {
+ expect(mockDelete).toHaveBeenCalledWith('1', options);
+ expect(result.current.isPending).toBe(false);
+ expect(result.current.isSuccess).toBe(true);
+ });
});
- await deletePromise;
+ it('should handle mutation error', async () => {
+ const mockError = new Error('Delete failed');
+ mockDelete.mockRejectedValue(mockError);
- return waitFor(() => {
- expect(result.current.isPending).toBe(false);
- expect(result.current.isSuccess).toBe(true);
+ const wrapper = createWrapper(mockPocketBase);
+
+ const { result } = renderHook(() => useDeleteMutation('test', '1'), { wrapper });
+
+ result.current.mutate();
+
+ await waitFor(() => {
+ expect(result.current.error).toEqual('Delete failed');
+ expect(result.current.isPending).toBe(false);
+ expect(result.current.isSuccess).toBe(false);
+ expect(result.current.isError).toBe(true);
+ });
});
- });
- it('should throw error when used outside provider', () => {
- expect(() => {
- renderHook(() => useDeleteMutation('test'));
- }).toThrow('usePocketBase must be used within a PocketBaseProvider');
+ it('should handle non-Error exceptions', async () => {
+ mockDelete.mockRejectedValue('String error');
+
+ const wrapper = createWrapper(mockPocketBase);
+
+ const { result } = renderHook(() => useDeleteMutation('test', '1'), { wrapper });
+
+ result.current.mutate();
+
+ await waitFor(() => {
+ expect(result.current.error).toBe('Error deleting record');
+ expect(result.current.isPending).toBe(false);
+ expect(result.current.isSuccess).toBe(false);
+ expect(result.current.isError).toBe(true);
+ });
+ });
});
});
diff --git a/tests/hooks/useRecord.test.tsx b/tests/hooks/useRecord.test.tsx
index 1d595c8..0325557 100644
--- a/tests/hooks/useRecord.test.tsx
+++ b/tests/hooks/useRecord.test.tsx
@@ -424,7 +424,7 @@ describe('useRecord', () => {
return Promise.resolve(() => {});
});
- const faultyTransformer = (record: RecordModel) => {
+ const faultyTransformer = (_record: RecordModel) => {
throw new Error('Transformer error');
};
@@ -477,7 +477,7 @@ describe('useRecord', () => {
const transformer2 = (record: RecordModel) => ({
...record,
- title: record.title + '!',
+ title: `${record.title}!`,
});
const wrapper = createWrapper(mockPocketBase);
diff --git a/tests/hooks/useUpdateMutation.test.tsx b/tests/hooks/useUpdateMutation.test.tsx
index d289617..a4e2056 100644
--- a/tests/hooks/useUpdateMutation.test.tsx
+++ b/tests/hooks/useUpdateMutation.test.tsx
@@ -6,6 +6,7 @@ import { createMockPocketBase, createWrapper, getMockCollectionMethods } from '.
describe('useUpdateMutation', () => {
let mockPocketBase: PocketBase;
+ // biome-ignore lint/suspicious/noExplicitAny: Mock function type
let mockUpdate: any;
beforeEach(() => {
@@ -17,27 +18,43 @@ describe('useUpdateMutation', () => {
it('should return initial state', () => {
const wrapper = createWrapper(mockPocketBase);
- const { result } = renderHook(() => useUpdateMutation('test'), { wrapper });
+ const { result } = renderHook(() => useUpdateMutation('test', '1'), { wrapper });
expect(result.current.isPending).toBe(false);
expect(result.current.error).toBe(null);
expect(result.current.isSuccess).toBe(true);
+ expect(result.current.isError).toBe(false);
expect(typeof result.current.mutate).toBe('function');
+ expect(typeof result.current.mutateAsync).toBe('function');
});
- it('should handle update mutation', async () => {
- const mockData = { id: '1', title: 'Updated Item' };
- mockUpdate.mockResolvedValue(mockData);
+ it('should throw error when used outside provider', () => {
+ expect(() => {
+ renderHook(() => useUpdateMutation('test', '1'));
+ }).toThrow('usePocketBase must be used within a PocketBaseProvider');
+ });
+ it('should handle null id', async () => {
const wrapper = createWrapper(mockPocketBase);
- const { result } = renderHook(() => useUpdateMutation('test'), { wrapper });
+ const { result } = renderHook(() => useUpdateMutation('test', null), { wrapper });
- const mutationResult = await result.current.mutate('1', {
- title: 'Updated Item',
- });
+ await expect(result.current.mutateAsync({ title: 'Test' })).rejects.toThrow('ID is required');
+ });
+
+ describe('mutateAsync', () => {
+ it('should handle successful update', async () => {
+ const mockData = { id: '1', title: 'Updated Item' };
+ mockUpdate.mockResolvedValue(mockData);
+
+ const wrapper = createWrapper(mockPocketBase);
+
+ const { result } = renderHook(() => useUpdateMutation('test', '1'), { wrapper });
+
+ const mutationResult = await result.current.mutateAsync({
+ title: 'Updated Item',
+ });
- return waitFor(() => {
expect(mockUpdate).toHaveBeenCalledWith('1', {
title: 'Updated Item',
});
@@ -45,86 +62,154 @@ describe('useUpdateMutation', () => {
expect(result.current.isPending).toBe(false);
expect(result.current.isSuccess).toBe(true);
});
- });
- it('should handle update mutation with options', async () => {
- const mockData = { id: '1', title: 'Updated Item' };
- mockUpdate.mockResolvedValue(mockData);
+ it('should handle update with options', async () => {
+ const mockData = { id: '1', title: 'Updated Item' };
+ mockUpdate.mockResolvedValue(mockData);
- const wrapper = createWrapper(mockPocketBase);
+ const wrapper = createWrapper(mockPocketBase);
- const { result } = renderHook(() => useUpdateMutation('test'), { wrapper });
+ const { result } = renderHook(() => useUpdateMutation('test', '1'), { wrapper });
- const options = { expand: 'relation' };
- await result.current.mutate('1', { title: 'Updated Item' }, options);
+ const options = { expand: 'relation' };
+ await result.current.mutateAsync({ title: 'Updated Item' }, options);
- return waitFor(() => {
expect(mockUpdate).toHaveBeenCalledWith('1', { title: 'Updated Item' }, options);
});
- });
- it('should handle mutation error', async () => {
- const mockError = new Error('Update failed');
- mockUpdate.mockRejectedValue(mockError);
+ it('should handle mutation error', async () => {
+ const mockError = new Error('Update failed');
+ mockUpdate.mockRejectedValue(mockError);
- const wrapper = createWrapper(mockPocketBase);
+ const wrapper = createWrapper(mockPocketBase);
- const { result } = renderHook(() => useUpdateMutation('test'), { wrapper });
+ const { result } = renderHook(() => useUpdateMutation('test', '1'), { wrapper });
- await result.current.mutate('1', { title: 'Test' });
+ await expect(result.current.mutateAsync({ title: 'Test' })).rejects.toThrow('Update failed');
- return waitFor(() => {
- expect(result.current.error).toEqual('Update failed');
- expect(result.current.isPending).toBe(false);
- expect(result.current.isSuccess).toBe(false);
+ await waitFor(() => {
+ expect(result.current.error).toEqual('Update failed');
+ expect(result.current.isPending).toBe(false);
+ expect(result.current.isSuccess).toBe(false);
+ expect(result.current.isError).toBe(true);
+ });
});
- });
- it('should handle non-Error exceptions', async () => {
- mockUpdate.mockRejectedValue('String error');
+ it('should handle non-Error exceptions', async () => {
+ mockUpdate.mockRejectedValue('String error');
- const wrapper = createWrapper(mockPocketBase);
+ const wrapper = createWrapper(mockPocketBase);
- const { result } = renderHook(() => useUpdateMutation('test'), { wrapper });
+ const { result } = renderHook(() => useUpdateMutation('test', '1'), { wrapper });
- await result.current.mutate('1', { title: 'Test' });
+ await expect(result.current.mutateAsync({ title: 'Test' })).rejects.toThrow('Error updating record');
- return waitFor(() => {
- expect(result.current.error).toBe('Error updating record');
- expect(result.current.isPending).toBe(false);
- expect(result.current.isSuccess).toBe(false);
+ await waitFor(() => {
+ expect(result.current.error).toBe('Error updating record');
+ expect(result.current.isPending).toBe(false);
+ expect(result.current.isSuccess).toBe(false);
+ expect(result.current.isError).toBe(true);
+ });
+ });
+
+ it('should set isPending to true during mutation', async () => {
+ let resolveUpdate: (value: unknown) => void = () => {};
+ const updatePromise = new Promise((resolve) => {
+ resolveUpdate = resolve;
+ });
+ mockUpdate.mockReturnValue(updatePromise);
+
+ const wrapper = createWrapper(mockPocketBase);
+
+ const { result } = renderHook(() => useUpdateMutation('test', '1'), { wrapper });
+
+ const mutationPromise = result.current.mutateAsync({ title: 'Test' });
+
+ await waitFor(() => {
+ expect(result.current.isPending).toBe(true);
+ expect(result.current.isSuccess).toBe(false);
+ });
+
+ resolveUpdate({ id: '1', title: 'Test' });
+ await mutationPromise;
+
+ await waitFor(() => {
+ expect(result.current.isPending).toBe(false);
+ expect(result.current.isSuccess).toBe(true);
+ });
});
});
- it('should set isPending to true during mutation', async () => {
- let resolveUpdate: (value: unknown) => void;
- const updatePromise = new Promise((resolve) => {
- resolveUpdate = resolve;
+ describe('mutate', () => {
+ it('should handle successful update', async () => {
+ const mockData = { id: '1', title: 'Updated Item' };
+ mockUpdate.mockResolvedValue(mockData);
+
+ const wrapper = createWrapper(mockPocketBase);
+
+ const { result } = renderHook(() => useUpdateMutation('test', '1'), { wrapper });
+
+ const mutateResult = result.current.mutate({ title: 'Updated Item' });
+ expect(mutateResult).toBeUndefined();
+
+ await waitFor(() => {
+ expect(mockUpdate).toHaveBeenCalledWith('1', { title: 'Updated Item' });
+ expect(result.current.isPending).toBe(false);
+ expect(result.current.isSuccess).toBe(true);
+ });
});
- mockUpdate.mockReturnValue(updatePromise);
- const wrapper = createWrapper(mockPocketBase);
+ it('should handle update with options', async () => {
+ const mockData = { id: '1', title: 'Updated Item' };
+ mockUpdate.mockResolvedValue(mockData);
- const { result } = renderHook(() => useUpdateMutation('test'), { wrapper });
+ const wrapper = createWrapper(mockPocketBase);
- waitFor(() => {
- result.current.mutate('1', { title: 'Test' });
- expect(result.current.isPending).toBe(true);
- expect(result.current.isSuccess).toBe(false);
- resolveUpdate({ id: '1', title: 'Test' });
+ const { result } = renderHook(() => useUpdateMutation('test', '1'), { wrapper });
+
+ const options = { expand: 'relation' };
+ result.current.mutate({ title: 'Updated Item' }, options);
+
+ await waitFor(() => {
+ expect(mockUpdate).toHaveBeenCalledWith('1', { title: 'Updated Item' }, options);
+ expect(result.current.isPending).toBe(false);
+ expect(result.current.isSuccess).toBe(true);
+ });
});
- await updatePromise;
+ it('should handle mutation error', async () => {
+ const mockError = new Error('Update failed');
+ mockUpdate.mockRejectedValue(mockError);
- return waitFor(() => {
- expect(result.current.isPending).toBe(false);
- expect(result.current.isSuccess).toBe(true);
+ const wrapper = createWrapper(mockPocketBase);
+
+ const { result } = renderHook(() => useUpdateMutation('test', '1'), { wrapper });
+
+ result.current.mutate({ title: 'Test' });
+
+ await waitFor(() => {
+ expect(result.current.error).toEqual('Update failed');
+ expect(result.current.isPending).toBe(false);
+ expect(result.current.isSuccess).toBe(false);
+ expect(result.current.isError).toBe(true);
+ });
});
- });
- it('should throw error when used outside provider', () => {
- expect(() => {
- renderHook(() => useUpdateMutation('test'));
- }).toThrow('usePocketBase must be used within a PocketBaseProvider');
+ it('should handle non-Error exceptions', async () => {
+ mockUpdate.mockRejectedValue('String error');
+
+ const wrapper = createWrapper(mockPocketBase);
+
+ const { result } = renderHook(() => useUpdateMutation('test', '1'), { wrapper });
+
+ result.current.mutate({ title: 'Test' });
+
+ await waitFor(() => {
+ expect(result.current.error).toBe('Error updating record');
+ expect(result.current.isPending).toBe(false);
+ expect(result.current.isSuccess).toBe(false);
+ expect(result.current.isError).toBe(true);
+ });
+ });
});
});