Files
shiftcraft/.claude/skills/pocketbase-best-practices/rules/realtime-events.md
2026-04-17 23:26:01 +00:00

173 lines
4.6 KiB
Markdown

---
title: Handle Realtime Events Properly
impact: MEDIUM
impactDescription: Consistent UI state, proper optimistic updates
tags: realtime, events, state-management, ui
---
## Handle Realtime Events Properly
Realtime events should update local state correctly, handle edge cases, and maintain UI consistency.
**Incorrect (naive event handling):**
```javascript
// Blindly appending creates - may add duplicates
pb.collection('posts').subscribe('*', (e) => {
if (e.action === 'create') {
posts.push(e.record); // Might already exist from optimistic update!
}
});
// Not handling own actions
pb.collection('posts').subscribe('*', (e) => {
// User creates post -> optimistic update
// Realtime event arrives -> duplicate!
setPosts(prev => [...prev, e.record]);
});
// Missing action types
pb.collection('posts').subscribe('*', (e) => {
if (e.action === 'create') handleCreate(e);
// Ignoring update and delete!
});
```
**Correct (robust event handling):**
```javascript
// Handle all action types with deduplication
function useRealtimePosts() {
const [posts, setPosts] = useState([]);
const pendingCreates = useRef(new Set());
useEffect(() => {
loadPosts();
const unsub = pb.collection('posts').subscribe('*', (e) => {
switch (e.action) {
case 'create':
// Skip if we created it (optimistic update already applied)
if (pendingCreates.current.has(e.record.id)) {
pendingCreates.current.delete(e.record.id);
return;
}
setPosts(prev => {
// Deduplicate - might already exist
if (prev.some(p => p.id === e.record.id)) return prev;
return [e.record, ...prev];
});
break;
case 'update':
setPosts(prev => prev.map(p =>
p.id === e.record.id ? e.record : p
));
break;
case 'delete':
setPosts(prev => prev.filter(p => p.id !== e.record.id));
break;
}
});
return unsub;
}, []);
async function createPost(data) {
// Optimistic update
const tempId = `temp_${Date.now()}`;
const optimisticPost = { ...data, id: tempId };
setPosts(prev => [optimisticPost, ...prev]);
try {
const created = await pb.collection('posts').create(data);
// Mark as pending so realtime event is ignored
pendingCreates.current.add(created.id);
// Replace optimistic with real
setPosts(prev => prev.map(p =>
p.id === tempId ? created : p
));
return created;
} catch (error) {
// Rollback optimistic update
setPosts(prev => prev.filter(p => p.id !== tempId));
throw error;
}
}
return { posts, createPost };
}
// Batched updates for high-frequency changes
function useRealtimeWithBatching() {
const [posts, setPosts] = useState([]);
const pendingUpdates = useRef([]);
const flushTimeout = useRef(null);
useEffect(() => {
const unsub = pb.collection('posts').subscribe('*', (e) => {
pendingUpdates.current.push(e);
// Batch updates every 100ms
if (!flushTimeout.current) {
flushTimeout.current = setTimeout(() => {
flushUpdates();
flushTimeout.current = null;
}, 100);
}
});
return () => {
unsub();
if (flushTimeout.current) clearTimeout(flushTimeout.current);
};
}, []);
function flushUpdates() {
const updates = pendingUpdates.current;
pendingUpdates.current = [];
setPosts(prev => {
let next = [...prev];
for (const e of updates) {
if (e.action === 'create') {
if (!next.some(p => p.id === e.record.id)) {
next.unshift(e.record);
}
} else if (e.action === 'update') {
next = next.map(p => p.id === e.record.id ? e.record : p);
} else if (e.action === 'delete') {
next = next.filter(p => p.id !== e.record.id);
}
}
return next;
});
}
}
```
**Filtering events:**
```javascript
// Only handle events matching certain criteria
pb.collection('posts').subscribe('*', (e) => {
// Only published posts
if (e.record.status !== 'published') return;
// Only posts by current user
if (e.record.author !== pb.authStore.record?.id) return;
handleEvent(e);
});
// Subscribe with expand to get related data
pb.collection('posts').subscribe('*', (e) => {
// Note: expand data is included in realtime events
// if the subscription options include expand
console.log(e.record.expand?.author?.name);
}, { expand: 'author' });
```
Reference: [PocketBase Realtime Events](https://pocketbase.io/docs/api-realtime/)