Skip to content

useupdatemutation usedeletemutation should include an id#9

Merged
KevinBonnoron merged 3 commits into
mainfrom
8-useupdatemutation-usedeletemutation-should-include-an-id
Oct 23, 2025
Merged

useupdatemutation usedeletemutation should include an id#9
KevinBonnoron merged 3 commits into
mainfrom
8-useupdatemutation-usedeletemutation-should-include-an-id

Conversation

@KevinBonnoron

@KevinBonnoron KevinBonnoron commented Oct 23, 2025

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features
    • Mutation hooks now expose awaitable async methods and an explicit error flag; UIs updated to await results (e.g., confirm-before-delete, “Deleting…” state).
  • Bug Fixes
    • More consistent pending/error handling across create, update, and delete flows; inline error messages and button disabled styling improved.
  • Documentation
    • Examples and docs updated for async hook usage, trimmed inputs, and adjusted form/header text.
  • Tests
    • Expanded coverage for async mutation paths, pending transitions, and detailed error scenarios.

@KevinBonnoron KevinBonnoron linked an issue Oct 23, 2025 that may be closed by this pull request
@coderabbitai

coderabbitai Bot commented Oct 23, 2025

Copy link
Copy Markdown

Walkthrough

Mutation hooks now expose Promise-based APIs (mutateAsync) plus synchronous wrappers (mutate); update/delete hooks accept a nullable record id at initialization and validate it. Mutation common option types were added and query-common type renamed; examples and tests updated to new signatures and error/pending semantics.

Changes

Cohort / File(s) Summary
Mutation hook implementations
src/hooks/useCreateMutation.ts, src/hooks/useUpdateMutation.ts, src/hooks/useDeleteMutation.ts
Added mutateAsync (throws on error) and a void mutate wrapper delegating to it. useUpdateMutation and useDeleteMutation accept an id parameter (nullable) and validate presence. Error state now sets error and exposes isError; isSuccess computed as !isPending && !error.
Mutation type definitions
src/types/useMutationCommon.type.ts, src/types/useCreateMutation.type.ts, src/types/useUpdateMutation.type.ts, src/types/useDeleteMutation.type.ts
Introduced UseMutationCommonOptions (isPending/isSuccess/isError/error). Mutation result interfaces now extend it, add mutateAsync signatures, and change mutate to a void-triggering API; docs updated.
Query-common renames & public index
src/types/useQueryCommon.type.ts, src/types/useCollection.type.ts, src/types/useRecord.type.ts, src/types/index.ts
Renamed UseCommonOptionsUseQueryCommonOptions, updated imports/usages to the new name, and adjusted src/types/index.ts re-exports accordingly.
Examples
examples/useCreateMutation-example.tsx, examples/useUpdateMutation-example.tsx, examples/useDeleteMutation-example.tsx
Updated to destructure and await mutateAsync, added isError usage, passed editing/deleting id into hooks, added input trimming/validation and confirm-delete flow; button/UI behaviors adjusted for isPending/isError.
Tests
tests/hooks/useCreateMutation.test.tsx, tests/hooks/useUpdateMutation.test.tsx, tests/hooks/useDeleteMutation.test.tsx, tests/hooks/useCollection.test.tsx, tests/hooks/useRecord.test.tsx, tests/hooks/...
Tests updated to call hooks with new id param where required, exercise mutateAsync (success, provider-guard, null-id, non-Error rejections), assert isPending/isSuccess/isError transitions, and include minor test cleanups (unused param renames, string formatting).
Examples & usage samples (minor UI)
examples/...
UI text and minor layout tweaks in examples to reflect new flow and inline error rendering.

Pre-merge checks and finishing touches

Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 error)
Check name Status Explanation Resolution
Docstring Coverage ❌ Error Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The pull request title "useupdatemutation usedeletemutation should include an id" directly and accurately describes the primary objective of the changeset. The core focus of this PR is to add an id parameter to both the useUpdateMutation and useDeleteMutation hooks, which is reflected consistently across all modified files (hook implementations, type definitions, examples, and tests). The title is clear and specific enough that someone reviewing the repository history would immediately understand the main change, without needing to list every detail such as the addition of mutateAsync, isError flags, or type restructuring—all of which are consequences of this primary architectural decision.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 8-useupdatemutation-usedeletemutation-should-include-an-id

Comment @coderabbitai help to get the list of available commands and usage tips.

@KevinBonnoron KevinBonnoron changed the title 8 useupdatemutation usedeletemutation should include an useupdatemutation usedeletemutation should include an id Oct 23, 2025

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (6)
tests/hooks/useDeleteMutation.test.tsx (2)

35-41: Null id path covered; consider mirroring for mutate().

Add a similar case that calls mutate() (non-async) with a null id to ensure error state updates without throwing.


9-15: Avoid any for mocks; infer from helper.

Type the mock from getMockCollectionMethods to keep strictness.

-  let mockDelete: any;
+  type CollectionMethods = ReturnType<typeof getMockCollectionMethods>;
+  let mockDelete: CollectionMethods['delete'];

As per coding guidelines.

src/hooks/useDeleteMutation.ts (4)

31-50: Prevent state updates after unmount (StrictMode-friendly).

Wrap updates with an isMounted ref to avoid React warnings if the component unmounts mid‑mutation.

-import { useCallback, useMemo, useState } from 'react';
+import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
@@
-  const mutateAsync = useCallback(
+  const isMounted = useRef(true);
+  useEffect(() => () => { isMounted.current = false; }, []);
+
+  const mutateAsync = useCallback(
     async (options?: CommonOptions): Promise<void> => {
@@
-      try {
-        setIsPending(true);
-        setError(null);
-        await recordService.delete(id, options);
-      } catch (err) {
-        const errorMessage = err instanceof Error ? err.message : 'Error deleting record';
-        setError(errorMessage);
-        throw new Error(errorMessage);
-      } finally {
-        setIsPending(false);
-      }
+      try {
+        if (isMounted.current) { setIsPending(true); setError(null); }
+        await recordService.delete(id, options);
+      } catch (err) {
+        const errorMessage = err instanceof Error ? err.message : 'Error deleting record';
+        if (isMounted.current) { setError(errorMessage); }
+        throw new Error(errorMessage);
+      } finally {
+        if (isMounted.current) { setIsPending(false); }
+      }
     },
     [recordService, id],
   );

6-24: Update the JSDoc example to match the new API.

Example still shows per‑call id and boolean return. Update to pass id at init and use mutateAsync() returning Promise<void>.

Also applies to: 10-10


25-25: Consider renaming the generic to avoid shadowing Record<…>.

Record is a well‑known TS utility. Prefer TRecord for clarity.

-export function useDeleteMutation<Record extends RecordModel = RecordModel>(collectionName: string, id: Record['id'] | null): UseDeleteMutationResult {
+export function useDeleteMutation<TRecord extends RecordModel = RecordModel>(collectionName: string, id: TRecord['id'] | null): UseDeleteMutationResult {

31-50: Optional: support request cancellation via requestKey.

If options?.requestKey is provided, expose a cancel() or internally call pb.cancelRequest(key) on unmount.

Based on coding guidelines.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0765843 and dae2330.

📒 Files selected for processing (10)
  • examples/useDeleteMutation-example.tsx (2 hunks)
  • examples/useUpdateMutation-example.tsx (2 hunks)
  • src/hooks/useDeleteMutation.ts (3 hunks)
  • src/hooks/useUpdateMutation.ts (2 hunks)
  • src/types/useDeleteMutation.type.ts (1 hunks)
  • src/types/useUpdateMutation.type.ts (1 hunks)
  • tests/hooks/useCollection.test.tsx (1 hunks)
  • tests/hooks/useDeleteMutation.test.tsx (1 hunks)
  • tests/hooks/useRecord.test.tsx (2 hunks)
  • tests/hooks/useUpdateMutation.test.tsx (2 hunks)
🧰 Additional context used
📓 Path-based instructions (6)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: TypeScript strictness: avoid any; prefer unknown when type is uncertain
Never add comments in code

**/*.{ts,tsx}: Enable strict typing and avoid any; prefer unknown when necessary
Use proper type guards to ensure runtime type safety

Files:

  • examples/useDeleteMutation-example.tsx
  • tests/hooks/useRecord.test.tsx
  • tests/hooks/useCollection.test.tsx
  • tests/hooks/useDeleteMutation.test.tsx
  • src/types/useDeleteMutation.type.ts
  • src/hooks/useUpdateMutation.ts
  • examples/useUpdateMutation-example.tsx
  • tests/hooks/useUpdateMutation.test.tsx
  • src/hooks/useDeleteMutation.ts
  • src/types/useUpdateMutation.type.ts
examples/**/*

📄 CodeRabbit inference engine (.cursorrules)

Maintain examples directory with basic/advanced usage and TypeScript imports

Files:

  • examples/useDeleteMutation-example.tsx
  • examples/useUpdateMutation-example.tsx
tests/hooks/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

tests/hooks/**/*.{test,spec}.{ts,tsx}: Write hook tests mirroring the hooks structure under tests/hooks/
Mock PocketBase client methods with Vitest vi.fn() and test loading, success, and error states
Verify real-time subscription setup and cleanup in tests
Use renderHook from @testing-library/react and wrap hooks with PocketBaseProvider using a mocked client
Use waitFor for assertions only: do not pass an async callback; perform actions before waitFor; always return/await the waitFor promise

Files:

  • tests/hooks/useRecord.test.tsx
  • tests/hooks/useCollection.test.tsx
  • tests/hooks/useDeleteMutation.test.tsx
  • tests/hooks/useUpdateMutation.test.tsx
tests/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursorrules)

tests/**/*.{ts,tsx}: Use Vitest as the test runner
Use React Testing Library’s renderHook for hook tests
Do not manually mock console.error in tests (handled by Vitest config)
Mock PocketBase methods comprehensively in tests
Test all hook states: loading, success, error

Files:

  • tests/hooks/useRecord.test.tsx
  • tests/hooks/useCollection.test.tsx
  • tests/hooks/useDeleteMutation.test.tsx
  • tests/hooks/useUpdateMutation.test.tsx
src/hooks/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

src/hooks/**/*.{ts,tsx}: All data-fetching hooks must return a discriminated union QueryResult with isLoading, isSuccess, isError, data, and error states
Hooks must set up PocketBase real-time subscriptions via pb.collection().subscribe() and clean up subscriptions in useEffect
Support request cancellation by accepting requestKey and using pb.cancelRequest(key) where applicable
Use TanStack Query-style naming: isLoading/isSuccess/isError for query hooks; isPending for mutations; fields data and error
Use useEffect for data fetching/subscriptions and always return a cleanup function for subscriptions
Memoize fetcher functions passed into useEffect with useCallback and avoid inline object/array creation in dependency arrays
Implement React StrictMode cancellation pattern: useRef for default values and a cancelled flag in async effects to prevent stale updates
In subscription handlers, use functional setState form setData(current => ...) to avoid adding data to effect dependencies
Provide generic type parameters for records in hooks, e.g., useCollection() and useAuth()
Place all custom hooks (useAuth, useCollection, useRecord, useCreateMutation, useUpdateMutation, useDeleteMutation, usePocketBase) in src/hooks/

src/hooks/**/*.{ts,tsx}: Follow TanStack Query state names in hooks: use isLoading, isSuccess, isError, error, data (not isPending)
Use signIn/signOut for auth hooks (not login/logout) and signUp (not register)
Implement real-time subscriptions in hooks with proper cleanup
Use useEffect for side effects; do not use useCallback for data fetching
Ensure hooks surface graceful error states and meaningful error messages
Ensure cleanup of real-time subscriptions to prevent memory leaks

Files:

  • src/hooks/useUpdateMutation.ts
  • src/hooks/useDeleteMutation.ts
src/hooks/{useCreateMutation,useUpdateMutation,useDeleteMutation}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Mutation hooks must expose mutate() and state flags isPending, isSuccess, isError, error

Files:

  • src/hooks/useUpdateMutation.ts
  • src/hooks/useDeleteMutation.ts
🧠 Learnings (8)
📓 Common learnings
Learnt from: CR
PR: KevinBonnoron/pocketbase-react-hooks#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-18T12:22:11.405Z
Learning: Applies to src/hooks/{useCreateMutation,useUpdateMutation,useDeleteMutation}.{ts,tsx} : Mutation hooks must expose mutate() and state flags isPending, isSuccess, isError, error
📚 Learning: 2025-10-18T12:22:11.405Z
Learnt from: CR
PR: KevinBonnoron/pocketbase-react-hooks#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-18T12:22:11.405Z
Learning: Applies to src/hooks/{useCreateMutation,useUpdateMutation,useDeleteMutation}.{ts,tsx} : Mutation hooks must expose mutate() and state flags isPending, isSuccess, isError, error

Applied to files:

  • examples/useDeleteMutation-example.tsx
  • src/hooks/useUpdateMutation.ts
  • examples/useUpdateMutation-example.tsx
  • tests/hooks/useUpdateMutation.test.tsx
📚 Learning: 2025-10-18T12:22:11.405Z
Learnt from: CR
PR: KevinBonnoron/pocketbase-react-hooks#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-18T12:22:11.405Z
Learning: Applies to src/hooks/{useCollection,useRecord}.{ts,tsx} : useCollection and useRecord must support a transformers option (array of RecordTransformer<T>) applied sequentially (reduce) to initial fetch and real-time updates; default include dateTransformer()

Applied to files:

  • tests/hooks/useRecord.test.tsx
  • tests/hooks/useCollection.test.tsx
📚 Learning: 2025-10-18T12:22:11.405Z
Learnt from: CR
PR: KevinBonnoron/pocketbase-react-hooks#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-18T12:22:11.405Z
Learning: Applies to src/lib/utils.ts : applyTransformers() must catch transformer errors, log to console, and return the original record to avoid breaking flow

Applied to files:

  • tests/hooks/useRecord.test.tsx
  • tests/hooks/useCollection.test.tsx
📚 Learning: 2025-10-18T12:22:11.405Z
Learnt from: CR
PR: KevinBonnoron/pocketbase-react-hooks#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-18T12:22:11.405Z
Learning: Applies to src/hooks/{useCollection,useRecord}.{ts,tsx} : By default, apply dateTransformer() to convert created/updated fields to Date objects; allow custom transformers or none via transformers: []

Applied to files:

  • tests/hooks/useCollection.test.tsx
📚 Learning: 2025-10-18T12:22:11.405Z
Learnt from: CR
PR: KevinBonnoron/pocketbase-react-hooks#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-18T12:22:11.405Z
Learning: Applies to tests/hooks/**/*.{test,spec}.{ts,tsx} : Use renderHook from testing-library/react and wrap hooks with PocketBaseProvider using a mocked client

Applied to files:

  • tests/hooks/useDeleteMutation.test.tsx
  • tests/hooks/useUpdateMutation.test.tsx
📚 Learning: 2025-10-18T12:22:42.057Z
Learnt from: CR
PR: KevinBonnoron/pocketbase-react-hooks#0
File: .cursorrules:0-0
Timestamp: 2025-10-18T12:22:42.057Z
Learning: Applies to tests/**/*.{ts,tsx} : Mock PocketBase methods comprehensively in tests

Applied to files:

  • tests/hooks/useDeleteMutation.test.tsx
  • tests/hooks/useUpdateMutation.test.tsx
📚 Learning: 2025-10-18T12:22:11.405Z
Learnt from: CR
PR: KevinBonnoron/pocketbase-react-hooks#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-18T12:22:11.405Z
Learning: Applies to tests/hooks/**/*.{test,spec}.{ts,tsx} : Mock PocketBase client methods with Vitest vi.fn() and test loading, success, and error states

Applied to files:

  • tests/hooks/useDeleteMutation.test.tsx
  • tests/hooks/useUpdateMutation.test.tsx
🧬 Code graph analysis (7)
examples/useDeleteMutation-example.tsx (1)
src/hooks/useDeleteMutation.ts (1)
  • useDeleteMutation (25-71)
tests/hooks/useRecord.test.tsx (1)
tests/test-utils.tsx (1)
  • record (86-88)
tests/hooks/useDeleteMutation.test.tsx (2)
src/hooks/useDeleteMutation.ts (1)
  • useDeleteMutation (25-71)
tests/test-utils.tsx (1)
  • createWrapper (114-116)
src/hooks/useUpdateMutation.ts (2)
src/types/useUpdateMutation.type.ts (1)
  • UseUpdateMutationResult (8-39)
src/hooks/usePocketBase.ts (1)
  • usePocketBase (24-31)
examples/useUpdateMutation-example.tsx (1)
src/hooks/useUpdateMutation.ts (1)
  • useUpdateMutation (26-73)
tests/hooks/useUpdateMutation.test.tsx (2)
src/hooks/useUpdateMutation.ts (1)
  • useUpdateMutation (26-73)
tests/test-utils.tsx (1)
  • createWrapper (114-116)
src/hooks/useDeleteMutation.ts (2)
src/types/useDeleteMutation.type.ts (1)
  • UseDeleteMutationResult (6-35)
src/hooks/usePocketBase.ts (1)
  • usePocketBase (24-31)
🔇 Additional comments (21)
tests/hooks/useCollection.test.tsx (1)

741-741: LGTM! Unused parameter convention applied.

Prefixing the unused parameter with an underscore is a good practice that signals the parameter is intentionally ignored while the transformer throws an error.

src/types/useUpdateMutation.type.ts (1)

10-23: LGTM! API surface correctly updated.

The type definitions properly reflect the new API where id is provided at hook initialization and both synchronous (mutate) and asynchronous (mutateAsync) variants are exposed.

src/hooks/useUpdateMutation.ts (4)

26-26: LGTM! Hook signature correctly updated.

The addition of the id parameter aligns with the PR objectives and provides better encapsulation of the record being updated.


32-52: LGTM! Solid implementation with proper validation and error handling.

The mutateAsync implementation correctly:

  • Validates id presence before attempting update
  • Manages isPending state through try-finally
  • Handles both Error and non-Error exceptions
  • Rethrows errors after setting state for caller handling

54-61: LGTM! Proper fire-and-forget wrapper.

The mutate function correctly wraps mutateAsync and swallows errors since they're already handled and stored in state. This provides the synchronous API while maintaining error observability through the error state.


63-72: LGTM! Return value and dependencies correctly updated.

The memoized return value properly includes both mutate and mutateAsync, and the dependency array correctly includes id since mutateAsync depends on it.

tests/hooks/useRecord.test.tsx (2)

427-427: LGTM! Unused parameter convention applied.

Prefixing with underscore correctly signals the parameter is intentionally unused in the faulty transformer.


480-480: LGTM! Modern syntax improvement.

Using template literals is more readable and consistent with modern JavaScript conventions.

examples/useUpdateMutation-example.tsx (2)

29-33: LGTM! Correctly using the new API.

The example properly demonstrates the updated mutateAsync call with only bodyParams, relying on the id passed during hook initialization.


19-19: No cleanup or request cancellation logic exists; this is the library's current design pattern.

Verification found no AbortController, AbortSignal, or useEffect cleanup patterns in useUpdateMutation or similar mutation hooks (useCreateMutation, useDeleteMutation). When editingId transitions (null → value → null), pending mutations continue in the background without cancellation. However, the example handles this implicitly: the form hides when editingId is null, preventing stale state from displaying. If cleanup is required, add a useEffect to cancel requests when id changes, but this would need to be applied consistently across all mutation hooks.

tests/hooks/useUpdateMutation.test.tsx (5)

18-28: LGTM! Initial state verification includes new API.

The test correctly verifies both mutate and mutateAsync are exposed as functions, aligning with the new API surface.


30-34: LGTM! Provider guard tested.

Good addition to verify the hook throws when used outside the provider context.


36-42: LGTM! ID validation tested.

The test correctly verifies that mutateAsync throws when id is null, which is the expected behavior per the implementation.


44-138: LGTM! Comprehensive mutateAsync test coverage.

The test suite thoroughly covers:

  • Successful updates with and without options
  • Error handling for Error and non-Error exceptions
  • isPending state transitions during mutation lifecycle

140-209: LGTM! Complete mutate test coverage.

The test suite properly verifies the synchronous mutate wrapper including success paths, options, and error handling scenarios.

src/types/useDeleteMutation.type.ts (1)

8-19: LGTM! Type definitions correctly updated.

The changes mirror the useUpdateMutation pattern, providing both synchronous (mutate) and asynchronous (mutateAsync) APIs with clear documentation.

examples/useDeleteMutation-example.tsx (2)

19-19: LGTM! Correctly using the new API.

The hook invocation properly passes deletingId to scope the deletion, and mutateAsync is correctly destructured for async operation handling.


33-37: LGTM! Confirmation logic properly encapsulated.

The confirmDelete function provides a clean separation between UI confirmation and mutation logic.

tests/hooks/useDeleteMutation.test.tsx (3)

20-27: API usage and initial state assertions look correct.

New signature and mutate/mutateAsync presence verified. LGTM.


29-33: Provider guard test is solid.

Covers the expected error message. LGTM.


43-57: Mutation flows (success, options, error, non-Error, pending) are well covered.

Good coverage of state transitions and error propagation. Once the hook exposes isError, add assertions for it (false on success, true on error).

If you add isError to the hook, update these tests to assert it. I can provide a follow‑up diff.

Also applies to: 58-69, 71-86, 88-101, 104-129

Comment thread examples/useDeleteMutation-example.tsx
Comment thread src/hooks/useDeleteMutation.ts
Comment thread src/hooks/useDeleteMutation.ts
Repository owner deleted a comment from coderabbitai Bot Oct 23, 2025
@KevinBonnoron

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Oct 23, 2025

Copy link
Copy Markdown
✅ Actions performed

Full review triggered.

1 similar comment
@coderabbitai

coderabbitai Bot commented Oct 23, 2025

Copy link
Copy Markdown
✅ Actions performed

Full review triggered.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/hooks/useUpdateMutation.ts (1)

6-25: Update JSDoc example and description to match the new API.

Docs still show mutate taking id; the hook now accepts id at initialization and mutate/mutateAsync take body/options only.

Apply:

@@
- * @param id - The ID of the record to update
+ * @param id - The ID of the record to update
@@
- * const { mutate, isPending, isSuccess, error } = useUpdateMutation<Post>('posts');
+ * const { mutateAsync, isPending, isSuccess, error } = useUpdateMutation<Post>('posts', 'record_id');
@@
- *   const updated = await mutate('record_id', { title: 'Updated Title' });
+ *   const updated = await mutateAsync({ title: 'Updated Title' });
src/hooks/useDeleteMutation.ts (1)

13-24: Update JSDoc example for the new id-based API.

Example still shows mutate taking id; switch to id at hook init and mutateAsync() with options.

@@
- * const { mutate, isPending, isSuccess, error } = useDeleteMutation('posts');
+ * const { mutateAsync, isPending, isSuccess, error } = useDeleteMutation('posts', 'record_id');
@@
- *   const success = await mutate('record_id');
- *   if (success) {
- *     console.log('Deleted successfully');
- *   }
+ *   await mutateAsync();
+ *   console.log('Deleted successfully');

Also applies to: 25-26

♻️ Duplicate comments (2)
examples/useDeleteMutation-example.tsx (1)

21-35: Race condition: shared isPending state across id changes.

This issue was previously flagged and remains unresolved. When deletingId changes (line 26), the hook reinitializes with a new id, but the previous mutation's finally block (line 33) will still execute and set deletingId to null. This creates multiple problems:

  1. Concurrent deletions: If user rapidly clicks delete on posts A then B:

    • Post A's finally executes → setDeletingId(null)
    • Hook reinitializes with id=null
    • Post B's mutation still running but isPending from the new hook instance may be incorrect
  2. Premature null assignment: The finally block always sets deletingId(null) even though another deletion might be queued or in progress.

  3. Hook reinitialization overhead: Each deletingId change triggers a complete hook reinitialization, discarding the previous hook's state.

Recommended fix: Track pending state per post ID rather than using a single shared state:

-const [deletingId, setDeletingId] = useState<string | null>(null);
-const { mutateAsync: deletePost, isPending, isSuccess, isError, error } = useDeleteMutation('posts', deletingId);
+const [deletingIds, setDeletingIds] = useState<Set<string>>(new Set());
+const { mutateAsync: deletePost, isError, error } = useDeleteMutation('posts', null);

 const handleDelete = async (postId: string) => {
-  if (isPending) {
+  if (deletingIds.has(postId)) {
     return;
   }

-  setDeletingId(postId);
+  setDeletingIds(prev => new Set(prev).add(postId));
   try {
-    await deletePost();
+    // Pass id to a version of the hook that accepts it at mutation time,
+    // or refactor to use a single hook instance that accepts id in mutateAsync
     console.log('Post deleted successfully');
   } catch (err) {
     console.error('Failed to delete post:', err);
   } finally {
-    setDeletingId(null);
+    setDeletingIds(prev => {
+      const next = new Set(prev);
+      next.delete(postId);
+      return next;
+    });
   }
 };

Alternative: If the hook API requires id at initialization and cannot accept it at mutation time, consider using separate hook instances per deletion or implementing abort logic when id changes.

src/hooks/useDeleteMutation.ts (1)

52-59: Remove the inline comment in the empty catch.

Matches a prior review; keep the silent catch but drop the comment.

-  mutateAsync(options).catch(() => {
-    // Error is already handled in mutateAsync
-  });
+  mutateAsync(options).catch(() => {});

As per coding guidelines.

🧹 Nitpick comments (6)
src/hooks/useCreateMutation.ts (1)

33-49: Simplify options handling.

Line 38 uses an unnecessary conditional to pass the options parameter. Since PocketBase's create() method already accepts options as an optional parameter, you can simplify by always passing it:

-const record = options ? await recordService.create(bodyParams, options) : await recordService.create(bodyParams);
+const record = await recordService.create(bodyParams, options);

The error handling (lines 40-43) correctly converts non-Error exceptions and re-throws after updating state, which is appropriate for the mutateAsync pattern.

tests/hooks/useCreateMutation.test.tsx (1)

25-28: Good coverage; add two tiny assertions and one follow-up test.

  • While pending, also assert error === null and isError === false to lock the contract.
  • After an error, add a subsequent-success test to ensure error/isError reset.
  • Optionally type mocks as vi.Mock for better IntelliSense.

Apply examples:

@@
   await waitFor(() => {
-    expect(result.current.isPending).toBe(true);
-    expect(result.current.isSuccess).toBe(false);
+    expect(result.current.isPending).toBe(true);
+    expect(result.current.isSuccess).toBe(false);
+    expect(result.current.isError).toBe(false);
+    expect(result.current.error).toBeNull();
   });

New follow-up test (mutateAsync block):

@@ describe('mutateAsync', () => {
+  it('should clear error on subsequent success', async () => {
+    const wrapper = createWrapper(mockPocketBase);
+    const { result } = renderHook(() => useCreateMutation('test'), { wrapper });
+    mockCreate.mockRejectedValueOnce(new Error('boom'));
+    await expect(result.current.mutateAsync({ title: 'A' })).rejects.toThrow('boom');
+    await waitFor(() => {
+      expect(result.current.isError).toBe(true);
+      expect(result.current.error).toBe('boom');
+    });
+    const ok = { id: '2', title: 'B' };
+    mockCreate.mockResolvedValueOnce(ok);
+    const out = await result.current.mutateAsync({ title: 'B' });
+    expect(out).toEqual(ok);
+    await waitFor(() => {
+      expect(result.current.isPending).toBe(false);
+      expect(result.current.isSuccess).toBe(true);
+      expect(result.current.isError).toBe(false);
+      expect(result.current.error).toBeNull();
+    });
+  });

And (optional) type the mock:

- let mockCreate: any;
+ import type { Mock } from 'vitest';
+ let mockCreate: Mock;

Based on learnings.

Also applies to: 30-34, 36-54, 55-68, 69-85, 87-103, 104-129, 132-149, 151-167, 169-185, 187-203

src/hooks/useUpdateMutation.ts (2)

32-52: Preserve original error stack and simplify the update call.

  • Keep the original Error when possible to retain stack context.
  • You can call update with options as undefined; no need for the ternary.
   const mutateAsync = useCallback(
     async (bodyParams: Partial<Record>, options?: RecordOptions): Promise<Record> => {
       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);
+        const record = await recordService.update(id, bodyParams, options);
         return record as Record;
       } catch (err) {
-        const errorMessage = err instanceof Error ? err.message : 'Error updating record';
-        setError(errorMessage);
-        throw new Error(errorMessage);
+        const errorMessage = err instanceof Error ? err.message : 'Error updating record';
+        setError(errorMessage);
+        if (err instanceof Error) throw err;
+        throw new Error(errorMessage);
       } finally {
         setIsPending(false);
       }
     },
     [recordService, id],
   );

63-73: Optionally compute isSuccess from isError for clarity.

Not required, but improves readability and avoids repeating the negation logic.

-    (): UseUpdateMutationResult<Record> => ({
+    (): UseUpdateMutationResult<Record> => ({
       mutate,
       mutateAsync,
       isPending,
-      isError: !!error,
-      error,
-      isSuccess: !isPending && !error,
+      isError: !!error,
+      error,
+      isSuccess: !isPending && !Boolean(error),
     }),
tests/hooks/useDeleteMutation.test.tsx (1)

20-28: Tests look solid; add mutate(null-id) and error-reset checks.

  • Add a test for mutate() when id is null to ensure state reflects the error (since mutate swallows throws).
  • Add a follow-up success after a failure to verify error/isError reset.

Examples:

@@
 it('should handle null id', async () => {
   const wrapper = createWrapper(mockPocketBase);
   const { result } = renderHook(() => useDeleteMutation('test', null), { wrapper });
   await expect(result.current.mutateAsync()).rejects.toThrow('ID is required');
 });
+
+it('should surface error state when mutate is called with null id', async () => {
+  const wrapper = createWrapper(mockPocketBase);
+  const { result } = renderHook(() => useDeleteMutation('test', null), { wrapper });
+  result.current.mutate();
+  await waitFor(() => {
+    expect(result.current.error).toBe('ID is required');
+    expect(result.current.isError).toBe(true);
+    expect(result.current.isSuccess).toBe(false);
+  });
+});

Also optional typing:

- let mockDelete: any;
+ import type { Mock } from 'vitest';
+ let mockDelete: Mock;

Based on learnings.

Also applies to: 30-43, 44-71, 72-89, 90-106, 107-133, 135-168, 170-203

src/hooks/useDeleteMutation.ts (1)

31-50: Preserve original error stack when possible.

Rethrow the original Error to keep the stack; otherwise construct a new Error.

   const mutateAsync = useCallback(
     async (options?: CommonOptions): Promise<void> => {
       if (!id) {
         throw new Error('ID is required');
       }
@@
-      } catch (err) {
-        const errorMessage = err instanceof Error ? err.message : 'Error deleting record';
-        setError(errorMessage);
-        throw new Error(errorMessage);
+      } catch (err) {
+        const errorMessage = err instanceof Error ? err.message : 'Error deleting record';
+        setError(errorMessage);
+        if (err instanceof Error) throw err;
+        throw new Error(errorMessage);
       } finally {
         setIsPending(false);
       }
     },
     [recordService, id],
   );
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dae2330 and eeaf31b.

📒 Files selected for processing (17)
  • examples/useCreateMutation-example.tsx (2 hunks)
  • examples/useDeleteMutation-example.tsx (2 hunks)
  • examples/useUpdateMutation-example.tsx (4 hunks)
  • src/hooks/useCreateMutation.ts (2 hunks)
  • src/hooks/useDeleteMutation.ts (3 hunks)
  • src/hooks/useUpdateMutation.ts (2 hunks)
  • src/types/index.ts (1 hunks)
  • src/types/useCollection.type.ts (1 hunks)
  • src/types/useCreateMutation.type.ts (1 hunks)
  • src/types/useDeleteMutation.type.ts (1 hunks)
  • src/types/useMutationCommon.type.ts (1 hunks)
  • src/types/useQueryCommon.type.ts (1 hunks)
  • src/types/useRecord.type.ts (1 hunks)
  • src/types/useUpdateMutation.type.ts (1 hunks)
  • tests/hooks/useCreateMutation.test.tsx (1 hunks)
  • tests/hooks/useDeleteMutation.test.tsx (1 hunks)
  • tests/hooks/useUpdateMutation.test.tsx (2 hunks)
🧰 Additional context used
🧠 Learnings (8)
📓 Common learnings
Learnt from: CR
PR: KevinBonnoron/pocketbase-react-hooks#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-18T12:22:11.405Z
Learning: Applies to src/hooks/{useCreateMutation,useUpdateMutation,useDeleteMutation}.{ts,tsx} : Mutation hooks must expose mutate() and state flags isPending, isSuccess, isError, error
📚 Learning: 2025-10-18T12:22:11.405Z
Learnt from: CR
PR: KevinBonnoron/pocketbase-react-hooks#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-18T12:22:11.405Z
Learning: Applies to src/hooks/{useCreateMutation,useUpdateMutation,useDeleteMutation}.{ts,tsx} : Mutation hooks must expose mutate() and state flags isPending, isSuccess, isError, error

Applied to files:

  • src/types/useCreateMutation.type.ts
  • tests/hooks/useCreateMutation.test.tsx
  • src/types/useDeleteMutation.type.ts
  • examples/useDeleteMutation-example.tsx
  • src/hooks/useDeleteMutation.ts
  • tests/hooks/useUpdateMutation.test.tsx
  • src/hooks/useCreateMutation.ts
  • examples/useUpdateMutation-example.tsx
  • src/types/useMutationCommon.type.ts
  • examples/useCreateMutation-example.tsx
  • src/hooks/useUpdateMutation.ts
📚 Learning: 2025-10-18T12:22:11.405Z
Learnt from: CR
PR: KevinBonnoron/pocketbase-react-hooks#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-18T12:22:11.405Z
Learning: Applies to src/hooks/**/*.{ts,tsx} : Provide generic type parameters for records in hooks, e.g., useCollection<Post>() and useAuth<User>()

Applied to files:

  • src/types/useRecord.type.ts
  • src/types/useCollection.type.ts
📚 Learning: 2025-10-18T12:22:11.405Z
Learnt from: CR
PR: KevinBonnoron/pocketbase-react-hooks#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-18T12:22:11.405Z
Learning: Applies to tests/hooks/**/*.{test,spec}.{ts,tsx} : Use renderHook from testing-library/react and wrap hooks with PocketBaseProvider using a mocked client

Applied to files:

  • tests/hooks/useCreateMutation.test.tsx
  • tests/hooks/useUpdateMutation.test.tsx
  • tests/hooks/useDeleteMutation.test.tsx
📚 Learning: 2025-10-18T12:22:42.057Z
Learnt from: CR
PR: KevinBonnoron/pocketbase-react-hooks#0
File: .cursorrules:0-0
Timestamp: 2025-10-18T12:22:42.057Z
Learning: Applies to tests/**/*.{ts,tsx} : Mock PocketBase methods comprehensively in tests

Applied to files:

  • tests/hooks/useCreateMutation.test.tsx
  • tests/hooks/useUpdateMutation.test.tsx
  • tests/hooks/useDeleteMutation.test.tsx
📚 Learning: 2025-10-18T12:22:11.405Z
Learnt from: CR
PR: KevinBonnoron/pocketbase-react-hooks#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-18T12:22:11.405Z
Learning: Applies to tests/hooks/**/*.{test,spec}.{ts,tsx} : Mock PocketBase client methods with Vitest vi.fn() and test loading, success, and error states

Applied to files:

  • tests/hooks/useCreateMutation.test.tsx
  • tests/hooks/useUpdateMutation.test.tsx
  • tests/hooks/useDeleteMutation.test.tsx
📚 Learning: 2025-10-18T12:22:11.405Z
Learnt from: CR
PR: KevinBonnoron/pocketbase-react-hooks#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-18T12:22:11.405Z
Learning: Applies to src/hooks/{useCollection,useRecord}.{ts,tsx} : useCollection and useRecord must support a transformers option (array of RecordTransformer<T>) applied sequentially (reduce) to initial fetch and real-time updates; default include dateTransformer()

Applied to files:

  • src/types/useQueryCommon.type.ts
  • src/types/useCollection.type.ts
📚 Learning: 2025-10-18T12:22:11.405Z
Learnt from: CR
PR: KevinBonnoron/pocketbase-react-hooks#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-18T12:22:11.405Z
Learning: Applies to src/hooks/{useCollection,useRecord}.{ts,tsx} : Support request cancellation via the requestKey option in useCollection and useRecord

Applied to files:

  • src/types/useCollection.type.ts
🔇 Additional comments (19)
src/types/useQueryCommon.type.ts (1)

4-19: LGTM! Improved naming clarity.

The rename from UseCommonOptions to UseQueryCommonOptions better reflects the purpose of this interface for query-related hooks. The separation of query and mutation common options improves type organization.

src/types/useRecord.type.ts (1)

3-10: LGTM! Consistent with type refactoring.

The update to use UseQueryCommonOptions aligns with the broader refactoring and maintains consistency across query-related hooks.

src/types/useCreateMutation.type.ts (2)

24-24: LGTM! Promise-based async mutation added.

The addition of mutateAsync provides a promise-based API for users who need to await the mutation result. This complements the fire-and-forget mutate function.


9-16: Breaking change verification complete—no issues found.

The codebase correctly implements the pattern where mutate() returns void (fire-and-forget) while mutateAsync() returns a Promise<Record> for async operations. All three pattern searches confirmed no code awaits mutate(), assigns its return value, or chains .then() on it. The test suite validates both methods exist, and the example demonstrates proper usage of mutateAsync() for Promise-based workflows. The implementation follows standard React Query conventions.

examples/useCreateMutation-example.tsx (2)

25-25: LGTM! Good input validation.

The trim check prevents blank submissions and improves UX.


28-36: LGTM! Proper async mutation handling.

The use of mutateAsync with try-catch provides good error handling, and resetting all form fields (including status) after successful creation improves UX.

src/types/useMutationCommon.type.ts (1)

1-21: LGTM! Well-structured common mutation state.

The interface properly captures the standard mutation state flags (isPending, isSuccess, isError, error) and aligns with established patterns. Clear documentation for each property enhances usability.

Based on learnings.

examples/useUpdateMutation-example.tsx (2)

29-33: LGTM! Simplified update payload.

The updated API where the id is passed to the hook initialization (line 19) and the mutation receives only the data payload is cleaner and aligns with the PR objective.


19-19: Hook appropriately handles null id with explicit error.

Verification complete. The useUpdateMutation hook accepts null id and explicitly checks for it, throwing an error "ID is required" when mutateAsync is called with null id. The example defensively checks !editingId before submission, preventing the error case. Behavior is documented and appropriate.

src/types/useCollection.type.ts (1)

3-10: LGTM! Consistent with type refactoring.

The update to use UseQueryCommonOptions maintains consistency across all query-related hooks (useCollection, useRecord).

src/types/index.ts (1)

8-8: Export migration verified and complete.

The export change from './useCommon.type' to './useQueryCommon.type' has been properly implemented:

  • Old file removed
  • New file exists with correct export
  • All internal references (useRecord.type.ts, useCollection.type.ts) updated to import from new location
  • No broken imports or dangling references remain
src/types/useUpdateMutation.type.ts (1)

1-25: LGTM! Type definitions align with the new initialization-time id API.

The type definitions correctly reflect the new API pattern where the record id is provided at hook initialization rather than at mutation time. The separation of mutate (synchronous trigger) and mutateAsync (returns Promise) is well-documented and consistent with the UseMutationCommonOptions pattern.

src/types/useDeleteMutation.type.ts (1)

1-21: LGTM! Type definitions consistent with the mutation API pattern.

The type definitions follow the same pattern as UseUpdateMutationResult, with the id provided at initialization and mutate/mutateAsync methods that don't require the id parameter. The documentation clearly distinguishes synchronous and asynchronous behaviors.

tests/hooks/useUpdateMutation.test.tsx (1)

1-215: LGTM! Comprehensive test coverage for the new mutation API.

The test suite thoroughly covers:

  • Initial state validation including the new isError flag
  • Provider context requirement
  • Null id handling with appropriate error
  • Both mutate and mutateAsync code paths
  • Success scenarios with and without options
  • Error handling for both Error instances and non-Error exceptions
  • Pending state transitions during async operations

The tests align well with the updated API surface and provide strong confidence in the hook's behavior.

Based on learnings

examples/useDeleteMutation-example.tsx (1)

63-63: Verify button state with per-post pending logic.

The button currently uses a global isPending check (lines 63, 72), which becomes incorrect with the race condition described above. If you implement per-post pending tracking, update these checks to use the per-post state:

-disabled={isPending}
+disabled={deletingIds.has(post.id)}
-{isPending ? 'Deleting...' : 'Delete'}
+{deletingIds.has(post.id) ? 'Deleting...' : 'Delete'}

Also applies to: 72-72

src/hooks/useCreateMutation.ts (2)

51-58: LGTM! The synchronous mutate wrapper correctly delegates to mutateAsync.

The mutate function appropriately:

  • Delegates to mutateAsync for the actual mutation logic
  • Suppresses the unhandled promise rejection since mutateAsync already updates the error state
  • Provides a fire-and-forget API as documented

This pattern aligns with the mutation hook requirements and maintains a clean separation between synchronous and asynchronous mutation triggers.

Based on learnings


60-70: LGTM! Return object correctly exposes the mutation API.

The memoized return value properly includes:

  • Both mutate and mutateAsync for different usage patterns
  • State flags isPending, isSuccess, isError as required
  • error for error details
  • Correct dependency array including both mutation functions

Based on learnings

src/hooks/useDeleteMutation.ts (1)

61-71: Return shape looks consistent (isError included).

No changes needed; matches project conventions.

Based on learnings.

src/hooks/useUpdateMutation.ts (1)

26-26: Test coverage for null id path already exists—no action needed.

The requested test for useUpdateMutation with a null id is already implemented at lines 40–42 of tests/hooks/useUpdateMutation.test.tsx, and it mirrors the useDeleteMutation pattern exactly as suggested in the review comment.

Comment thread examples/useCreateMutation-example.tsx Outdated
Comment thread examples/useUpdateMutation-example.tsx Outdated
@KevinBonnoron KevinBonnoron force-pushed the 8-useupdatemutation-usedeletemutation-should-include-an-id branch from eeaf31b to 133ab95 Compare October 23, 2025 12:42
@KevinBonnoron KevinBonnoron merged commit e0bbd52 into main Oct 23, 2025
1 check passed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

♻️ Duplicate comments (1)
src/hooks/useDeleteMutation.ts (1)

52-59: Remove inline comment per coding guidelines.

The inline comment violates the "no code comments" guideline and was previously flagged. Keep the silent catch but remove the comment.

   const mutate = useCallback(
     (options?: CommonOptions): void => {
-      mutateAsync(options).catch(() => {
-        // Error is already handled in mutateAsync
-      });
+      mutateAsync(options).catch(() => {});
     },
     [mutateAsync],
   );

As per coding guidelines.

🧹 Nitpick comments (8)
src/types/useDeleteMutation.type.ts (1)

9-14: Clarify id flow in JSDoc.

Since id is now provided at hook initialization (not to mutate), add a note in this JSDoc to prevent misuse.

src/types/useCreateMutation.type.ts (1)

9-25: Avoid shadowing TS Record; rename generic to TRecord.

Improves readability and avoids confusion with Record<…, …>.

-export interface UseCreateMutationResult<Record extends RecordModel> extends UseMutationCommonOptions {
+export interface UseCreateMutationResult<TRecord extends RecordModel> extends UseMutationCommonOptions {
-  mutate: (bodyParams: Partial<Record>, options?: RecordOptions) => void;
+  mutate: (bodyParams: Partial<TRecord>, options?: RecordOptions) => void;
-  mutateAsync: (bodyParams: Partial<Record>, options?: RecordOptions) => Promise<Record>;
+  mutateAsync: (bodyParams: Partial<TRecord>, options?: RecordOptions) => Promise<TRecord>;
}
src/types/useUpdateMutation.type.ts (1)

9-25: Rename generic to TRecord for consistency and clarity.

Mirror the create type to avoid Record shadowing.

-export interface UseUpdateMutationResult<Record extends RecordModel> extends UseMutationCommonOptions {
+export interface UseUpdateMutationResult<TRecord extends RecordModel> extends UseMutationCommonOptions {
-  mutate: (bodyParams: Partial<Record>, options?: RecordOptions) => void;
+  mutate: (bodyParams: Partial<TRecord>, options?: RecordOptions) => void;
-  mutateAsync: (bodyParams: Partial<Record>, options?: RecordOptions) => Promise<Record>;
+  mutateAsync: (bodyParams: Partial<TRecord>, options?: RecordOptions) => Promise<TRecord>;
}
examples/useUpdateMutation-example.tsx (3)

28-38: Reset status after successful update to avoid stale UI.

       console.log('Post updated:', updatedPost);
       setEditingId(null);
       setEditTitle('');
       setEditContent('');
+      setEditStatus('draft');

50-54: Reset status on cancel for consistency.

   const cancelEdit = () => {
     setEditingId(null);
     setEditTitle('');
     setEditContent('');
+    setEditStatus('draft');
   };

78-80: Prevent accidental actions during in‑flight update.

Disable Cancel while pending.

-          <button type="button" onClick={cancelEdit}>
+          <button type="button" onClick={cancelEdit} disabled={isPending}>
             Cancel
           </button>
tests/hooks/useCreateMutation.test.tsx (1)

3-15: Type mocks as vi.Mock instead of any.

Improves type safety and removes the need for lint ignores elsewhere.

-import { beforeEach, describe, expect, it } from 'vitest';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
 ...
-  let mockCreate: any;
+  let mockCreate: vi.Mock;
tests/hooks/useUpdateMutation.test.tsx (1)

3-11: Replace any + biome ignore with proper mock typing.

Use vi.Mock and drop the suppressive comment.

-import { beforeEach, describe, expect, it } from 'vitest';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
-// biome-ignore lint/suspicious/noExplicitAny: Mock function type
-let mockUpdate: any;
+let mockUpdate: vi.Mock;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0765843 and 133ab95.

📒 Files selected for processing (19)
  • examples/useCreateMutation-example.tsx (2 hunks)
  • examples/useDeleteMutation-example.tsx (2 hunks)
  • examples/useUpdateMutation-example.tsx (3 hunks)
  • src/hooks/useCreateMutation.ts (2 hunks)
  • src/hooks/useDeleteMutation.ts (3 hunks)
  • src/hooks/useUpdateMutation.ts (2 hunks)
  • src/types/index.ts (1 hunks)
  • src/types/useCollection.type.ts (1 hunks)
  • src/types/useCreateMutation.type.ts (1 hunks)
  • src/types/useDeleteMutation.type.ts (1 hunks)
  • src/types/useMutationCommon.type.ts (1 hunks)
  • src/types/useQueryCommon.type.ts (1 hunks)
  • src/types/useRecord.type.ts (1 hunks)
  • src/types/useUpdateMutation.type.ts (1 hunks)
  • tests/hooks/useCollection.test.tsx (1 hunks)
  • tests/hooks/useCreateMutation.test.tsx (1 hunks)
  • tests/hooks/useDeleteMutation.test.tsx (1 hunks)
  • tests/hooks/useRecord.test.tsx (2 hunks)
  • tests/hooks/useUpdateMutation.test.tsx (2 hunks)
🧰 Additional context used
🧠 Learnings (11)
📓 Common learnings
Learnt from: CR
PR: KevinBonnoron/pocketbase-react-hooks#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-18T12:22:11.405Z
Learning: Applies to src/hooks/{useCreateMutation,useUpdateMutation,useDeleteMutation}.{ts,tsx} : Mutation hooks must expose mutate() and state flags isPending, isSuccess, isError, error
📚 Learning: 2025-10-18T12:22:11.405Z
Learnt from: CR
PR: KevinBonnoron/pocketbase-react-hooks#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-18T12:22:11.405Z
Learning: Applies to src/hooks/{useCollection,useRecord}.{ts,tsx} : useCollection and useRecord must support a transformers option (array of RecordTransformer<T>) applied sequentially (reduce) to initial fetch and real-time updates; default include dateTransformer()

Applied to files:

  • tests/hooks/useRecord.test.tsx
  • tests/hooks/useCollection.test.tsx
  • src/types/useQueryCommon.type.ts
  • src/types/useCollection.type.ts
📚 Learning: 2025-10-18T12:22:11.405Z
Learnt from: CR
PR: KevinBonnoron/pocketbase-react-hooks#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-18T12:22:11.405Z
Learning: Applies to src/lib/utils.ts : applyTransformers() must catch transformer errors, log to console, and return the original record to avoid breaking flow

Applied to files:

  • tests/hooks/useRecord.test.tsx
  • tests/hooks/useCollection.test.tsx
📚 Learning: 2025-10-18T12:22:11.405Z
Learnt from: CR
PR: KevinBonnoron/pocketbase-react-hooks#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-18T12:22:11.405Z
Learning: Applies to src/hooks/{useCollection,useRecord}.{ts,tsx} : By default, apply dateTransformer() to convert created/updated fields to Date objects; allow custom transformers or none via transformers: []

Applied to files:

  • tests/hooks/useRecord.test.tsx
  • tests/hooks/useCollection.test.tsx
📚 Learning: 2025-10-18T12:22:11.405Z
Learnt from: CR
PR: KevinBonnoron/pocketbase-react-hooks#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-18T12:22:11.405Z
Learning: Applies to src/hooks/{useCreateMutation,useUpdateMutation,useDeleteMutation}.{ts,tsx} : Mutation hooks must expose mutate() and state flags isPending, isSuccess, isError, error

Applied to files:

  • tests/hooks/useCreateMutation.test.tsx
  • src/types/useDeleteMutation.type.ts
  • src/hooks/useCreateMutation.ts
  • src/types/useMutationCommon.type.ts
  • src/hooks/useDeleteMutation.ts
  • examples/useCreateMutation-example.tsx
  • tests/hooks/useUpdateMutation.test.tsx
  • src/hooks/useUpdateMutation.ts
  • src/types/useCreateMutation.type.ts
📚 Learning: 2025-10-18T12:22:11.405Z
Learnt from: CR
PR: KevinBonnoron/pocketbase-react-hooks#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-18T12:22:11.405Z
Learning: Applies to tests/hooks/**/*.{test,spec}.{ts,tsx} : Use renderHook from testing-library/react and wrap hooks with PocketBaseProvider using a mocked client

Applied to files:

  • tests/hooks/useCreateMutation.test.tsx
  • tests/hooks/useDeleteMutation.test.tsx
  • tests/hooks/useUpdateMutation.test.tsx
📚 Learning: 2025-10-18T12:22:42.057Z
Learnt from: CR
PR: KevinBonnoron/pocketbase-react-hooks#0
File: .cursorrules:0-0
Timestamp: 2025-10-18T12:22:42.057Z
Learning: Applies to tests/**/*.{ts,tsx} : Mock PocketBase methods comprehensively in tests

Applied to files:

  • tests/hooks/useCreateMutation.test.tsx
  • tests/hooks/useDeleteMutation.test.tsx
  • tests/hooks/useUpdateMutation.test.tsx
📚 Learning: 2025-10-18T12:22:11.405Z
Learnt from: CR
PR: KevinBonnoron/pocketbase-react-hooks#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-18T12:22:11.405Z
Learning: Applies to tests/hooks/**/*.{test,spec}.{ts,tsx} : Mock PocketBase client methods with Vitest vi.fn() and test loading, success, and error states

Applied to files:

  • tests/hooks/useCreateMutation.test.tsx
  • tests/hooks/useDeleteMutation.test.tsx
  • tests/hooks/useUpdateMutation.test.tsx
📚 Learning: 2025-10-18T12:22:11.405Z
Learnt from: CR
PR: KevinBonnoron/pocketbase-react-hooks#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-18T12:22:11.405Z
Learning: Applies to src/hooks/**/*.{ts,tsx} : Provide generic type parameters for records in hooks, e.g., useCollection<Post>() and useAuth<User>()

Applied to files:

  • src/types/useRecord.type.ts
  • src/types/useCollection.type.ts
📚 Learning: 2025-10-18T12:22:11.405Z
Learnt from: CR
PR: KevinBonnoron/pocketbase-react-hooks#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-18T12:22:11.405Z
Learning: Applies to src/types/index.ts : All exported types must be explicitly defined in src/types/index.ts and referenced by hooks and utilities

Applied to files:

  • src/types/index.ts
📚 Learning: 2025-10-18T12:22:11.405Z
Learnt from: CR
PR: KevinBonnoron/pocketbase-react-hooks#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-18T12:22:11.405Z
Learning: Applies to src/hooks/{useCollection,useRecord}.{ts,tsx} : Support request cancellation via the requestKey option in useCollection and useRecord

Applied to files:

  • src/types/useCollection.type.ts
🔇 Additional comments (14)
tests/hooks/useCollection.test.tsx (1)

741-741: LGTM! Good code hygiene.

Prefixing the unused parameter with an underscore is a standard convention to indicate the parameter is intentionally unused, since the transformer immediately throws an error.

tests/hooks/useRecord.test.tsx (1)

427-427: LGTM! Good stylistic improvements.

Both changes improve code quality:

  • Prefixing unused parameter with underscore follows convention
  • Template literal is more idiomatic than string concatenation

Also applies to: 480-480

src/types/useQueryCommon.type.ts (1)

4-4: LGTM! Better semantic naming.

Renaming UseCommonOptions to UseQueryCommonOptions improves clarity by distinguishing query-specific options from mutation options, especially now that UseMutationCommonOptions has been introduced.

src/types/useRecord.type.ts (1)

3-3: LGTM! Consistent with type rename.

The import and extension changes correctly follow the rename from UseCommonOptions to UseQueryCommonOptions.

Also applies to: 10-10

src/types/useCollection.type.ts (1)

3-3: LGTM! Consistent with type rename.

The import and extension changes correctly follow the rename from UseCommonOptions to UseQueryCommonOptions.

Also applies to: 10-10

examples/useCreateMutation-example.tsx (3)

18-18: LGTM! Improved mutation API and validation.

Good improvements:

  • Using mutateAsync aligns with the new Promise-based mutation API
  • Adding isError to the destructured values provides explicit error state tracking
  • Input validation with trim() prevents empty submissions
  • Resetting status to 'draft' after creation maintains consistent UX

Also applies to: 25-26, 29-30, 36-36


44-44: Verify the heading hierarchy change is intentional.

The headings have been swapped:

  • Line 44 (outer): "Create Post" → "Create New Post"
  • Line 47 (form): "Create New Post" → "Create Post"

This reverses the hierarchy. Verify this is intentional and not an accidental swap during refactoring.

Also applies to: 47-47


64-64: LGTM! Improved error handling.

Moving error display to the form context (with isError && error guard) and removing the early return addresses the previous review feedback about redundant error handling. This provides better UX by showing errors inline within the form.

src/types/useMutationCommon.type.ts (1)

1-21: LGTM! Good abstraction for mutation state.

This interface provides a clean, reusable base for mutation result types. The properties align with the learning that mutation hooks must expose state flags isPending, isSuccess, isError, and error. Well-documented with clear JSDoc comments.

Based on learnings.

tests/hooks/useDeleteMutation.test.tsx (4)

17-34: LGTM: Initial state and provider tests are comprehensive.

Tests correctly verify:

  • All returned fields including new isError flag
  • Both mutate and mutateAsync function presence
  • Provider requirement with appropriate error message

36-42: LGTM: Null ID validation test is correct.

Properly validates that mutateAsync throws "ID is required" when id is null, covering an important edge case.


44-133: LGTM: Comprehensive mutateAsync test coverage.

Tests thoroughly cover:

  • Success path with and without options
  • Error handling (Error instances and non-Error values)
  • State transitions (isPending, isSuccess, isError)
  • Proper promise resolution/rejection

135-204: LGTM: mutate wrapper tests are thorough.

Tests correctly verify:

  • Fire-and-forget behavior (returns undefined)
  • Delegation to mutateAsync with proper arguments
  • Error handling without throwing
  • State updates via waitFor
src/hooks/useDeleteMutation.ts (1)

61-71: LGTM: Return object now includes required isError flag.

The previously missing isError field has been properly added and derived from the error state. The return object now correctly exposes all mutation state per project conventions.

Based on learnings.

Comment on lines 18 to +35
const [deletingId, setDeletingId] = useState<string | null>(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);
}
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Closure bug causes "ID is required" error on every delete.

The delete flow is broken due to closure capture timing:

  1. Initial render: deletingId=null → hook initialized with id=nulldeletePost will throw
  2. User clicks delete → handleDelete('A') captures current deletePost (with id=null)
  3. Line 26: setDeletingId('A') queues state update but doesn't take effect yet
  4. Line 28: await deletePost() calls the captured function → throws "ID is required" because it uses id=null

The state update happens after the function reference is captured, so deletePost() always uses id=null.

Solution: Move the ID into the function call rather than hook initialization:

-  const [deletingId, setDeletingId] = useState<string | null>(null);
-  const { mutateAsync: deletePost, isPending, isSuccess, isError, error } = useDeleteMutation('posts', deletingId);
+  const { mutateAsync: deletePost, isPending, isSuccess, isError, error } = useDeleteMutation('posts', null);

   const handleDelete = async (postId: string) => {
     if (isPending) {
       return;
     }

-    setDeletingId(postId);
     try {
-      await deletePost();
+      await deletePost(postId);
       console.log('Post deleted successfully');
     } catch (err) {
       console.error('Failed to delete post:', err);
-    } finally {
-      setDeletingId(null);
     }
   };

However, this requires updating the hook API to accept the ID as an argument to mutateAsync instead of at initialization. Alternatively, redesign the hook to not require pre-initialization with an ID.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In examples/useDeleteMutation-example.tsx around lines 18-35, the delete handler
captures a stale deletePost closure because the hook was initialized with
deletingId=null; change the pattern so the ID is passed when calling mutateAsync
rather than at hook initialization: update the example to call the mutation with
the id (e.g., await deletePost(postId)) and keep deletingId state only for
UI/loading purposes; then update the useDeleteMutation hook signature and types
to remove the required id parameter on init and accept the id argument in
mutateAsync (adjust internal logic to read the id from the mutate call), and
update all other call sites/tests to the new API accordingly.

Comment on lines +51 to +58
const mutate = useCallback(
(bodyParams: Partial<Record>, options?: RecordOptions): void => {
mutateAsync(bodyParams, options).catch(() => {
// Error is already handled in mutateAsync
});
},
[mutateAsync],
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Remove inline comment per coding guidelines.

The inline comment violates the "no code comments" guideline. Keep the silent catch but remove the comment.

   const mutate = useCallback(
     (bodyParams: Partial<Record>, options?: RecordOptions): void => {
-      mutateAsync(bodyParams, options).catch(() => {
-        // Error is already handled in mutateAsync
-      });
+      mutateAsync(bodyParams, options).catch(() => {});
     },
     [mutateAsync],
   );

As per coding guidelines.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const mutate = useCallback(
(bodyParams: Partial<Record>, options?: RecordOptions): void => {
mutateAsync(bodyParams, options).catch(() => {
// Error is already handled in mutateAsync
});
},
[mutateAsync],
);
const mutate = useCallback(
(bodyParams: Partial<Record>, options?: RecordOptions): void => {
mutateAsync(bodyParams, options).catch(() => {});
},
[mutateAsync],
);
🤖 Prompt for AI Agents
In src/hooks/useCreateMutation.ts around lines 51 to 58, remove the inline
comment inside the catch block that reads "// Error is already handled in
mutateAsync" while preserving the silent catch behavior; simply leave the catch
callback body empty (or use an empty statement) so the promise rejection is
swallowed without any inline comments, adhering to the "no code comments"
guideline.

Comment on lines +54 to 61
const mutate = useCallback(
(bodyParams: Partial<Record>, options?: RecordOptions): void => {
mutateAsync(bodyParams, options).catch(() => {
// Error is already handled in mutateAsync
});
},
[mutateAsync],
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Remove inline comment per coding guidelines.

The inline comment violates the "no code comments" guideline. Keep the silent catch but remove the comment.

   const mutate = useCallback(
     (bodyParams: Partial<Record>, options?: RecordOptions): void => {
-      mutateAsync(bodyParams, options).catch(() => {
-        // Error is already handled in mutateAsync
-      });
+      mutateAsync(bodyParams, options).catch(() => {});
     },
     [mutateAsync],
   );

As per coding guidelines.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const mutate = useCallback(
(bodyParams: Partial<Record>, options?: RecordOptions): void => {
mutateAsync(bodyParams, options).catch(() => {
// Error is already handled in mutateAsync
});
},
[mutateAsync],
);
const mutate = useCallback(
(bodyParams: Partial<Record>, options?: RecordOptions): void => {
mutateAsync(bodyParams, options).catch(() => {});
},
[mutateAsync],
);
🤖 Prompt for AI Agents
In src/hooks/useUpdateMutation.ts around lines 54 to 61 there is an inline
comment inside the catch block; remove that comment and leave the silent catch
body intact (i.e., keep .catch(() => { }) with an empty block), then reformat to
satisfy linting/formatting rules.

Comment thread src/types/index.ts
export * from './useCommon.type';
export * from './useCreateMutation.type';
export * from './useDeleteMutation.type';
export * from './useQueryCommon.type';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify the missing export for UseMutationCommonOptions.

The rename from useCommon.type to useQueryCommon.type is consistent with the interface rename. However, the new UseMutationCommonOptions interface introduced in src/types/useMutationCommon.type.ts is not exported from this index file.

According to the codebase learnings, all exported types must be explicitly defined in src/types/index.ts. Verify whether other mutation type files import UseMutationCommonOptions directly or expect it to be re-exported from here.

Based on learnings.

Run the following script to check imports of UseMutationCommonOptions:


🏁 Script executed:

#!/bin/bash
# Check if UseMutationCommonOptions is imported anywhere and whether the export is needed

echo "=== Checking imports of UseMutationCommonOptions ==="
rg -n "import.*UseMutationCommonOptions" --type=ts --type=tsx

echo -e "\n=== Checking if useMutationCommon.type is referenced ==="
rg -n "from.*useMutationCommon" --type=ts --type=tsx

Length of output: 166


[/run_scripts]


🏁 Script executed:

#!/bin/bash
# Check if UseMutationCommonOptions is imported anywhere and whether the export is needed

echo "=== Checking imports of UseMutationCommonOptions ==="
rg -n "import.*UseMutationCommonOptions" --type=ts

echo -e "\n=== Checking if useMutationCommon.type is referenced ==="
rg -n "from.*useMutationCommon" --type=ts

echo -e "\n=== Checking current exports in src/types/index.ts ==="
cat src/types/index.ts

echo -e "\n=== Checking if useMutationCommon.type file exists ==="
fd "useMutationCommon" src/types/

Length of output: 1317


Add missing export for useMutationCommon.type in src/types/index.ts.

The verification confirms the issue: UseMutationCommonOptions is imported directly by three mutation type files (useCreateMutation.type.ts, useDeleteMutation.type.ts, useUpdateMutation.type.ts), and all three of these files are exported from the index. However, src/types/index.ts does not export useMutationCommon.type itself, breaking the consistent export pattern where all type files are re-exported from the index.

Add export * from './useMutationCommon.type'; to src/types/index.ts to make UseMutationCommonOptions accessible to external consumers through the index file.

🤖 Prompt for AI Agents
In src/types/index.ts around line 8, the file currently exports
useQueryCommon.type but is missing an export for useMutationCommon.type; add the
missing re-export by adding export * from './useMutationCommon.type'; so
UseMutationCommonOptions and related types are available through the index
alongside the other type files.

Comment on lines +7 to 21
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<boolean>;
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<void>;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Confirm delete return type; align mutateAsync accordingly.

PocketBase’s delete traditionally returns no body (often boolean/void). Your mutateAsync returns Promise<void>. Please verify SDK behavior and adjust if it actually returns boolean to prevent downstream typing drift.


🌐 Web query:

In PocketBase 0.26.x JS SDK, what is the return type of collection.delete(id, options)?

💡 Result:

The method returns a Promise. [1][2]

Sources:
[1] Type definitions (pocketbase-js) showing RecordService.delete(...) : Promise.
[2] Third‑party TypeScript definitions for pocketbase JS SDK v0.26.x showing delete(...) : Promise.


Update mutateAsync return type to Promise<boolean> to match PocketBase SDK behavior.

PocketBase 0.26.x's collection.delete(id, options) returns Promise<boolean>, not void. The current signature returns Promise<void>, which creates a typing mismatch and prevents consumers from accessing the deletion result (whether it succeeded).

Update line 16 from:

mutateAsync: (options?: CommonOptions) => Promise<void>;

to:

mutateAsync: (options?: CommonOptions) => Promise<boolean>;
🤖 Prompt for AI Agents
In src/types/useDeleteMutation.type.ts around lines 7 to 21, the mutateAsync
signature incorrectly returns Promise<void> but PocketBase's collection.delete
returns Promise<boolean>; update the type so mutateAsync returns
Promise<boolean> instead of Promise<void>, ensuring callers can receive the
deletion result and the type matches the SDK.

@KevinBonnoron KevinBonnoron deleted the 8-useupdatemutation-usedeletemutation-should-include-an-id branch November 22, 2025 16:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

useUpdateMutation & useDeleteMutation should include an id

1 participant