173 lines
4.6 KiB
Markdown
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/)
|