Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 29 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,17 @@ This pattern follows TanStack Query conventions with `isLoading`, `isSuccess`, `

**Request Cancellation**: The `requestKey` parameter is passed directly to PocketBase SDK and can be used with `pb.cancelRequest(key)` to abort pending requests. Useful for handling search queries, preventing race conditions, or cleaning up on unmount.

**Data Transformers**: Both `useCollection` and `useRecord` support a `transformers` option that accepts an array of `RecordTransformer<T>` functions. Transformers are applied sequentially (via `reduce()`) to transform records before they're returned to components. By default, both hooks automatically apply `dateTransformer()` which converts `created` and `updated` fields from ISO strings to `Date` objects. Transformers are applied to:
- Initial fetch results
- Real-time subscription events (create, update)

Error handling: If a transformer throws, `applyTransformers()` catches the error, logs to console, and returns the original record. Transformers use `useRef` to maintain stable references and avoid re-creating subscriptions.

### Hook Architecture

**useCollection**: Fetches collection data with `getFullList()` or `getList()` based on `fetchAll` option. Handles real-time updates by applying create/update/delete actions to the current data array and re-sorting if needed.
**useCollection**: Fetches collection data with `getFullList()` or `getList()` based on `fetchAll` option. Handles real-time updates by applying create/update/delete actions to the current data array and re-sorting if needed. Applies transformers to all records (both initial fetch and real-time updates).

**useRecord**: Fetches a single record by ID using `getOne()` or by filter using `getFirstListItem()`. Subscribes to that specific record's changes.
**useRecord**: Fetches a single record by ID using `getOne()` or by filter using `getFirstListItem()`. Subscribes to that specific record's changes. Applies transformers to the record (both initial fetch and real-time updates).

Comment thread
KevinBonnoron marked this conversation as resolved.
**useAuth**: Manages authentication state by listening to `pb.authStore.onChange()`. Provides `signIn.email()`, `signIn.social()`, `signUp.email()`, and `signOut()` methods. Returns the authenticated user via `pb.authStore.model`.

Expand All @@ -66,7 +72,8 @@ This pattern follows TanStack Query conventions with `isLoading`, `isSuccess`, `
- `src/context/PocketBaseContext.tsx` - React Context definition and `usePocketBaseContext()` hook
- `src/providers/PocketBaseProvider.tsx` - Provider component that wraps the app
- `src/hooks/` - All custom hooks (`useAuth`, `useCollection`, `useRecord`, `useCreateMutation`, `useUpdateMutation`, `useDeleteMutation`, `usePocketBase`)
- `src/lib/utils.ts` - Shared utilities (`createQueryResult`, `sortRecords`)
- `src/lib/utils.ts` - Shared utilities (`sortRecords`, `applyTransformers`)
- `src/transformers/` - Built-in transformers (`dateTransformer`)
- `src/types/index.ts` - TypeScript type definitions for hook options and results
- `tests/hooks/` - Test files mirroring the hooks structure

Expand Down Expand Up @@ -121,7 +128,23 @@ This pattern follows TanStack Query conventions with `isLoading`, `isSuccess`, `
- Verify subscription setup and cleanup
- Use `renderHook` from `@testing-library/react` to test hooks
- Wrap hooks in `PocketBaseProvider` with mocked client

- Use `waitFor` from `@testing-library/react` for assertions only (not `act` + `setTimeout`):
- Do NOT pass an async callback.
- Perform actions (user events/hook calls) before waitFor.
- Always return or await the waitFor promise.
```typescript
// ✅ Correct
await result.current.mutate('1');
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});

// ❌ Incorrect
await waitFor(async () => {
await result.current.mutate('1');
expect(result.current.isSuccess).toBe(true);
});
```
### Code Style
- **NEVER add comments** to code (enforced by .cursorrules)
- Use Biome for formatting and linting
Expand All @@ -131,8 +154,9 @@ This pattern follows TanStack Query conventions with `isLoading`, `isSuccess`, `
## Important Notes

- **Peer Dependencies**: React >=19.0.0 and PocketBase ^0.26.2 must be installed by consumers
- **Real-time Subscriptions**: Enabled by default but can be disabled with `subscribe: false` option
- **Real-time Subscriptions**: Enabled by default but can be disabled with `realtime: false` option
- **Conditional Fetching**: Use `enabled: false` to disable data fetching (similar to TanStack Query)
- **Error Handling**: All hooks expose `isError` and `error` for graceful error states
- **Request Cancellation**: Use `requestKey` option in `useCollection` and `useRecord` to enable request cancellation via `pb.cancelRequest(key)`
- **Data Transformers**: By default, `useCollection` and `useRecord` apply `dateTransformer()` to convert `created` and `updated` fields to `Date` objects. Users can provide custom transformers via the `transformers` option or disable all transformations with `transformers: []`. Transformers are stored in `useRef` to maintain stable references and prevent re-renders.
- **React StrictMode**: The library handles React StrictMode's double-mounting correctly with cancellation flags to prevent auto-cancelled requests from updating state. If you encounter infinite loops or auto-cancellation issues, ensure dependencies are stable (use `useRef` for objects/arrays passed as options)
219 changes: 213 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,8 +235,9 @@ Fetches and manages a collection of data with real-time subscriptions.
- `defaultValue`: Default value while loading
- `enabled`: Boolean to enable/disable data fetching (default: `true`)
- `fetchAll`: Boolean to use `getFullList` (true) or `getList` (false) (default: `true`)
- `subscribe`: Boolean to enable/disable real-time subscriptions (default: `true`)
- `realtime`: Boolean to enable/disable real-time subscriptions (default: `true`)
- `requestKey`: Optional key passed to PocketBase for request cancellation (optional)
- `transformers`: Array of transformer functions to apply to records (default: `[dateTransformer()]`)

**Returns:**
- `data`: Array of records or null
Expand Down Expand Up @@ -336,7 +337,7 @@ import { useCollection } from 'pocketbase-react-hooks';
function StaticPostsList() {
const { data: posts, isLoading, isError, error } = useCollection('posts', {
filter: 'status = "published"',
subscribe: false // Disable real-time updates
realtime: false // Disable real-time updates
});

if (isLoading) return <div>Loading posts...</div>;
Expand Down Expand Up @@ -377,7 +378,7 @@ function AdvancedPostsList({
expand: 'author',
fields: 'id,title,content,author',
enabled: shouldFetch,
subscribe: enableRealtime
realtime: enableRealtime
});

if (!shouldFetch) return <div>Data fetching is disabled</div>;
Expand Down Expand Up @@ -411,6 +412,7 @@ Fetches and manages a single record with real-time updates. Can fetch by ID or b
- `fields`: Fields to return
- `defaultValue`: Default value while loading
- `requestKey`: Optional key passed to PocketBase for request cancellation (optional)
- `transformers`: Array of transformer functions to apply to the record (default: `[dateTransformer()]`)

**Returns:**
- `data`: Record object or null
Expand Down Expand Up @@ -627,7 +629,7 @@ interface Post extends RecordModel {
status: 'draft' | 'published' | 'archived';
author: string; // relation to users
tags: string[];
published_at?: string;
publishedAt?: Date;
}

// Use with custom types
Expand All @@ -640,15 +642,220 @@ const { mutate: updatePost } = useUpdateMutation<Post>('posts');
const { mutate: deletePost } = useDeleteMutation('posts');
```

## Data Transformers

The library includes a powerful data transformation system that allows you to automatically transform data received from PocketBase before it reaches your components.

### Default Date Transformation

By default, both `useCollection` and `useRecord` automatically apply a `dateTransformer` that converts ISO date strings to JavaScript `Date` objects for the `created` and `updated` fields.

```tsx
import { useCollection } from 'pocketbase-react-hooks';

interface Post extends RecordModel {
title: string;
content: string;
created: Date; // Automatically transformed from string to Date
updated: Date; // Automatically transformed from string to Date
}

function PostsList() {
const { data: posts } = useCollection<Post>('posts');

return (
<div>
{posts?.map(post => (
<div key={post.id}>
<h3>{post.title}</h3>
<p>Created: {post.created.toLocaleDateString()}</p>
<p>Updated: {post.updated.toLocaleString()}</p>
</div>
))}
</div>
);
}
```

### Custom Date Fields

You can configure the `dateTransformer` to transform additional date fields:

```tsx
import { useCollection, dateTransformer } from 'pocketbase-react-hooks';

interface Post extends RecordModel {
title: string;
publishedAt: Date;
created: Date;
updated: Date;
}

Comment thread
KevinBonnoron marked this conversation as resolved.
function PostsList() {
const { data: posts } = useCollection<Post>('posts', {
transformers: [
dateTransformer(['created', 'updated', 'publishedAt'])
]
});

return (
<div>
{posts?.map(post => (
<div key={post.id}>
<h3>{post.title}</h3>
<p>Published: {post.publishedAt.toLocaleDateString()}</p>
</div>
))}
</div>
);
}
```

### Custom Transformers

Create your own transformers to apply custom data transformations:

```tsx
import { useCollection, dateTransformer } from 'pocketbase-react-hooks';
import type { RecordTransformer } from 'pocketbase-react-hooks';

interface Post extends RecordModel {
title: string;
content: string;
status: 'draft' | 'published' | 'archived';
}

const uppercaseTransformer: RecordTransformer<Post> = (record) => ({
...record,
title: record.title.toUpperCase(),
});

const statusNormalizer: RecordTransformer<Post> = (record) => ({
...record,
status: record.status.toLowerCase() as 'draft' | 'published' | 'archived',
});

function PostsList() {
const { data: posts } = useCollection<Post>('posts', {
transformers: [
dateTransformer(),
uppercaseTransformer,
statusNormalizer,
]
});

return (
<div>
{posts?.map(post => (
<div key={post.id}>
<h3>{post.title}</h3>
<span>{post.status}</span>
</div>
))}
</div>
);
}
```

### Transformer Composition

Transformers are applied in sequence, allowing you to compose multiple transformations:

```tsx
import { useCollection, dateTransformer } from 'pocketbase-react-hooks';

const trimWhitespace: RecordTransformer<Post> = (record) => ({
...record,
title: record.title.trim(),
content: record.content.trim(),
});

const addComputedFields: RecordTransformer<Post> = (record) => ({
...record,
excerpt: record.content.substring(0, 100) + '...',
wordCount: record.content.split(' ').length,
});

function PostsList() {
const { data: posts } = useCollection<Post>('posts', {
transformers: [
dateTransformer(),
trimWhitespace,
addComputedFields,
]
});

return <div>{/* ... */}</div>;
}
```

### Disabling Transformers

If you don't want any transformations (including the default date transformer), pass an empty array:

```tsx
import { useCollection } from 'pocketbase-react-hooks';

function PostsList() {
const { data: posts } = useCollection('posts', {
transformers: [] // No transformations applied
});

return <div>{/* ... */}</div>;
}
```

### Error Handling

Transformers include built-in error handling. If a transformer throws an error, the original record is returned unchanged, and the error is logged to the console:

```tsx
const faultyTransformer: RecordTransformer<Post> = (record) => {
if (!record.title) {
throw new Error('Title is required');
}
return record;
};

function PostsList() {
const { data: posts } = useCollection<Post>('posts', {
transformers: [
dateTransformer(),
faultyTransformer, // If this fails, original record is returned
]
});

return <div>{/* ... */}</div>;
}
```

### Real-time Updates

Transformers are automatically applied to:
- Initial data fetch
- Real-time subscription events (create, update)

This ensures data consistency across all updates:

```tsx
function PostsList() {
const { data: posts } = useCollection<Post>('posts', {
transformers: [dateTransformer()],
});

return <div>{/* All posts have Date objects, even from real-time updates */}</div>;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
```

## Real-time Features

All hooks support real-time updates through PocketBase subscriptions:

- `useCollection` automatically updates when records are created, updated, or deleted (can be disabled with `subscribe: false`)
- `useCollection` automatically updates when records are created, updated, or deleted (can be disabled with `realtime: false`)
- `useRecord` automatically updates when the specific record changes
- `useAuth` automatically updates when authentication state changes

**Note:** Real-time subscriptions are enabled by default but can be disabled using the `subscribe` option for better performance when real-time updates are not needed.
**Note:** Real-time subscriptions are enabled by default but can be disabled using the `realtime` option for better performance when real-time updates are not needed.

## Request Cancellation with requestKey

Expand Down
2 changes: 1 addition & 1 deletion examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ First, here's an example of a PocketBase database schema you might use:
- `status` (select: draft, published, archived)
- `author` (relation to users)
- `tags` (json array)
- `published_at` (date, optional)
- `publishedAt` (date, optional)

**comments** (Regular collection)
- `content` (text, required)
Expand Down
Loading