5.0 KiB
5.0 KiB
title, impact, impactDescription, tags
| title | impact | impactDescription | tags |
|---|---|---|---|
| Implement Realtime Subscriptions Correctly | MEDIUM | Live updates without polling, reduced server load | realtime, subscriptions, sse, websocket |
Implement Realtime Subscriptions Correctly
PocketBase uses Server-Sent Events (SSE) for realtime updates. Proper subscription management prevents memory leaks and ensures reliable event delivery.
Incorrect (memory leaks and poor management):
// Missing unsubscribe - memory leak!
function PostList() {
useEffect(() => {
pb.collection('posts').subscribe('*', (e) => {
updatePosts(e);
});
// No cleanup - subscription persists forever!
}, []);
}
// Subscribing multiple times
function loadPosts() {
// Called on every render - creates duplicate subscriptions!
pb.collection('posts').subscribe('*', handleChange);
}
// Not handling reconnection
pb.collection('posts').subscribe('*', (e) => {
// Assumes connection is always stable
updateUI(e);
});
Correct (proper subscription management):
// React example with cleanup
function PostList() {
const [posts, setPosts] = useState([]);
useEffect(() => {
// Initial load
loadPosts();
// Subscribe to changes
const unsubscribe = pb.collection('posts').subscribe('*', (e) => {
if (e.action === 'create') {
setPosts(prev => [e.record, ...prev]);
} else if (e.action === 'update') {
setPosts(prev => prev.map(p =>
p.id === e.record.id ? e.record : p
));
} else if (e.action === 'delete') {
setPosts(prev => prev.filter(p => p.id !== e.record.id));
}
});
// Cleanup on unmount
return () => {
unsubscribe();
};
}, []);
async function loadPosts() {
const result = await pb.collection('posts').getList(1, 50);
setPosts(result.items);
}
return <PostListUI posts={posts} />;
}
// Subscribe to specific record
async function watchPost(postId) {
return pb.collection('posts').subscribe(postId, (e) => {
console.log('Post changed:', e.action, e.record);
});
}
// Subscribe to collection changes
async function watchAllPosts() {
return pb.collection('posts').subscribe('*', (e) => {
console.log(`Post ${e.action}:`, e.record.title);
});
}
// Handle connection events
pb.realtime.subscribe('PB_CONNECT', (e) => {
console.log('Realtime connected, client ID:', e.clientId);
// Re-sync data after reconnection
refreshData();
});
// Vanilla JS with proper cleanup
class PostManager {
unsubscribes = [];
async init() {
this.unsubscribes.push(
await pb.collection('posts').subscribe('*', this.handlePostChange)
);
this.unsubscribes.push(
await pb.collection('comments').subscribe('*', this.handleCommentChange)
);
}
destroy() {
this.unsubscribes.forEach(unsub => unsub());
this.unsubscribes = [];
}
handlePostChange = (e) => { /* ... */ };
handleCommentChange = (e) => { /* ... */ };
}
Subscription event structure:
pb.collection('posts').subscribe('*', (event) => {
event.action; // 'create' | 'update' | 'delete'
event.record; // The affected record
});
// Full event type
interface RealtimeEvent {
action: 'create' | 'update' | 'delete';
record: RecordModel;
}
Unsubscribe patterns:
// Unsubscribe from specific callback
const unsub = await pb.collection('posts').subscribe('*', callback);
unsub(); // Remove this specific subscription
// Unsubscribe from all subscriptions on a topic
pb.collection('posts').unsubscribe('*'); // All collection subs
pb.collection('posts').unsubscribe('RECORD_ID'); // Specific record
// Unsubscribe from all collection subscriptions
pb.collection('posts').unsubscribe();
// Unsubscribe from everything
pb.realtime.unsubscribe();
Performance considerations:
// Prefer specific record subscriptions over collection-wide when possible.
// subscribe('*') checks ListRule for every connected client on each change.
// subscribe(recordId) checks ViewRule -- fewer records to evaluate.
// For high-traffic collections, subscribe to specific records:
await pb.collection('orders').subscribe(orderId, handleOrderUpdate);
// Instead of: pb.collection('orders').subscribe('*', handleAllOrders);
// Use subscription options to reduce payload size (SDK v0.21+):
await pb.collection('posts').subscribe('*', handleChange, {
fields: 'id,title,updated', // Only receive specific fields
expand: 'author', // Include expanded relations
filter: 'status = "published"' // Only receive matching records
});
Subscription scope guidelines:
| Scenario | Recommended Scope |
|---|---|
| Watching a specific document | subscribe(recordId) |
| Chat room messages | subscribe('*') with filter for room |
| User notifications | subscribe('*') with filter for user |
| Admin dashboard | subscribe('*') (need to see all) |
| High-frequency data (IoT) | subscribe(recordId) per device |
Reference: PocketBase Realtime