Skip to the content.

Real-time Features

Firebase ORM provides powerful real-time capabilities that allow your applications to automatically sync with database changes. This guide covers how to implement real-time updates, handle different event types, and manage subscriptions effectively.

Overview

Firebase ORM’s real-time features are built on top of Firestore’s real-time listeners, providing:

Basic Real-time Listening

Listen to All Records

Listen to changes across all documents in a collection:

import { User } from './models/User';

// Listen to all user changes
const unsubscribe = User.onList((user) => {
  console.log('User updated:', user.name);
  console.log('User ID:', user.getId());
});

// Stop listening when done
// unsubscribe();

Listen to Specific Events

Handle different types of changes with event-specific listeners:

// Listen only to newly added users
const unsubscribeAdded = User.onList((user) => {
  console.log('New user added:', user.name);
  // Update UI to show new user
}, 'added');

// Listen only to user modifications
const unsubscribeModified = User.onList((user) => {
  console.log('User modified:', user.name);
  // Update existing UI element
}, 'modified');

// Listen only to user deletions
const unsubscribeRemoved = User.onList((user) => {
  console.log('User removed:', user.getId());
  // Remove from UI
}, 'removed');

Advanced Event Handling

Use the mode-based listener for complete control:

const unsubscribe = User.onModeList({
  // Called for newly added users
  added: (user) => {
    console.log('User added:', user.name);
    addUserToUI(user);
  },
  
  // Called when user data is modified
  modified: (user) => {
    console.log('User modified:', user.name);
    updateUserInUI(user);
  },
  
  // Called when user is deleted
  removed: (user) => {
    console.log('User removed:', user.getId());
    removeUserFromUI(user);
  },
  
  // Called during initial load
  init: (user) => {
    console.log('Initial user loaded:', user.name);
    addUserToUIWithoutAnimation(user);
  }
});

Single Document Listening

Listen to changes on a specific document:

// Load a specific user
const user = new User();
await user.load('user-id-here');

// Listen to changes on this specific user
const unsubscribe = user.on(() => {
  console.log('User data changed:', user.name);
  console.log('Updated email:', user.email);
  
  // The user object is automatically updated
  updateUserProfileInUI(user);
});

// Stop listening
// unsubscribe();

Framework Integration Examples

React Integration

// React hook for real-time data
import { useState, useEffect } from 'react';
import { User } from '../models/User';

export function useUsers() {
  const [users, setUsers] = useState<User[]>([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    // Initial load
    User.getAll().then(initialUsers => {
      setUsers(initialUsers);
      setLoading(false);
    });

    // Set up real-time listener
    const unsubscribe = User.onModeList({
      added: (user) => {
        setUsers(prev => [...prev, user]);
      },
      modified: (user) => {
        setUsers(prev => prev.map(u => 
          u.getId() === user.getId() ? user : u
        ));
      },
      removed: (user) => {
        setUsers(prev => prev.filter(u => 
          u.getId() !== user.getId()
        ));
      }
    });

    return () => unsubscribe();
  }, []);

  return { users, loading };
}

// Using the hook in a component
function UserList() {
  const { users, loading } = useUsers();

  if (loading) return <div>Loading...</div>;

  return (
    <div>
      {users.map(user => (
        <div key={user.getId()}>
          {user.name} - {user.email}
        </div>
      ))}
    </div>
  );
}

Vue.js Integration

<template>
  <div>
    <div v-if="loading">Loading...</div>
    <div v-else>
      <div v-for="user in users" :key="user.getId()">
         - 
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
import { User } from '../models/User';

const users = ref<User[]>([]);
const loading = ref(true);
let unsubscribe: (() => void) | null = null;

onMounted(async () => {
  // Initial load
  const initialUsers = await User.getAll();
  users.value = initialUsers;
  loading.value = false;

  // Set up real-time listener
  unsubscribe = User.onModeList({
    added: (user) => {
      users.value.push(user);
    },
    modified: (user) => {
      const index = users.value.findIndex(u => u.getId() === user.getId());
      if (index >= 0) {
        users.value[index] = user;
      }
    },
    removed: (user) => {
      users.value = users.value.filter(u => u.getId() !== user.getId());
    }
  });
});

onUnmounted(() => {
  if (unsubscribe) {
    unsubscribe();
  }
});
</script>

Angular Integration

// Angular service
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { User } from '../models/User';

@Injectable({
  providedIn: 'root'
})
export class UserService {
  private usersSubject = new BehaviorSubject<User[]>([]);
  public users$ = this.usersSubject.asObservable();
  private unsubscribe?: () => void;

  async initializeRealtime() {
    // Initial load
    const initialUsers = await User.getAll();
    this.usersSubject.next(initialUsers);

    // Set up real-time listener
    this.unsubscribe = User.onModeList({
      added: (user) => {
        const currentUsers = this.usersSubject.value;
        this.usersSubject.next([...currentUsers, user]);
      },
      modified: (user) => {
        const currentUsers = this.usersSubject.value;
        const updatedUsers = currentUsers.map(u => 
          u.getId() === user.getId() ? user : u
        );
        this.usersSubject.next(updatedUsers);
      },
      removed: (user) => {
        const currentUsers = this.usersSubject.value;
        const filteredUsers = currentUsers.filter(u => 
          u.getId() !== user.getId()
        );
        this.usersSubject.next(filteredUsers);
      }
    });
  }

  destroy() {
    if (this.unsubscribe) {
      this.unsubscribe();
    }
  }
}

// Angular component
import { Component, OnInit, OnDestroy } from '@angular/core';
import { UserService } from '../services/user.service';

@Component({
  selector: 'app-user-list',
  template: `
    <div *ngFor="let user of userService.users$ | async">
       - 
    </div>
  `
})
export class UserListComponent implements OnInit, OnDestroy {
  constructor(public userService: UserService) {}

  async ngOnInit() {
    await this.userService.initializeRealtime();
  }

  ngOnDestroy() {
    this.userService.destroy();
  }
}

Query-based Real-time Listening

Listen to real-time changes on filtered data:

// Listen to only active users
const unsubscribe = User.query()
  .where('isActive', '==', true)
  .onSnapshot((users) => {
    console.log('Active users updated:', users.length);
    users.forEach(user => {
      console.log('Active user:', user.name);
    });
  });

// Listen to recent posts
const unsubscribeRecentPosts = Post.query()
  .where('createdAt', '>', new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString())
  .orderBy('createdAt', 'desc')
  .limit(10)
  .onSnapshot((posts) => {
    console.log('Recent posts updated:', posts.length);
    updateRecentPostsUI(posts);
  });

Relationship-aware Real-time Updates

Handle real-time updates for related data:

// User model with posts relationship
@Model({
  reference_path: 'users',
  path_id: 'user_id'
})
export class User extends BaseModel {
  @Field({ is_required: true })
  public name!: string;

  @HasMany({ model: Post, foreignKey: 'author_id' })
  public posts?: Post[];

  // Method to set up real-time listening for user and their posts
  async setupRealtimeWithPosts() {
    // Listen to user changes
    const userUnsubscribe = this.on(() => {
      console.log('User updated:', this.name);
      updateUserUI(this);
    });

    // Listen to post changes for this user
    const postsUnsubscribe = Post.query()
      .where('author_id', '==', this.getId())
      .onSnapshot((posts) => {
        this.posts = posts;
        console.log(`${this.name} now has ${posts.length} posts`);
        updateUserPostsUI(this, posts);
      });

    // Return combined unsubscribe function
    return () => {
      userUnsubscribe();
      postsUnsubscribe();
    };
  }
}

// Usage
const user = new User();
await user.load('user-id');
const unsubscribe = await user.setupRealtimeWithPosts();

// Clean up when done
// unsubscribe();

Advanced Real-time Patterns

Debounced Updates

Prevent too many UI updates with debouncing:

class RealtimeManager {
  private updateQueue = new Map<string, User>();
  private updateTimeout?: NodeJS.Timeout;

  setupDebouncedUserListener() {
    return User.onList((user) => {
      // Add to queue
      this.updateQueue.set(user.getId(), user);
      
      // Clear existing timeout
      if (this.updateTimeout) {
        clearTimeout(this.updateTimeout);
      }
      
      // Set new timeout to batch updates
      this.updateTimeout = setTimeout(() => {
        this.processBatchedUpdates();
      }, 100); // 100ms debounce
    });
  }

  private processBatchedUpdates() {
    const users = Array.from(this.updateQueue.values());
    console.log(`Processing ${users.length} batched updates`);
    
    // Update UI with all changes at once
    updateUsersInUI(users);
    
    // Clear queue
    this.updateQueue.clear();
  }
}

Connection State Management

Handle offline/online scenarios:

class RealtimeService {
  private subscriptions: (() => void)[] = [];
  private isOnline = navigator.onLine;

  constructor() {
    // Listen to connection changes
    window.addEventListener('online', () => {
      console.log('Back online - reconnecting...');
      this.isOnline = true;
      this.reconnectAll();
    });

    window.addEventListener('offline', () => {
      console.log('Gone offline - pausing updates');
      this.isOnline = false;
      this.pauseAll();
    });
  }

  addSubscription(setupFn: () => (() => void)) {
    if (this.isOnline) {
      const unsubscribe = setupFn();
      this.subscriptions.push(unsubscribe);
      return unsubscribe;
    } else {
      // Queue for when we're back online
      const unsubscribe = () => {};
      this.subscriptions.push(() => setupFn());
      return unsubscribe;
    }
  }

  private reconnectAll() {
    // Reconnect all subscriptions
    this.subscriptions.forEach(setupFn => {
      setupFn();
    });
  }

  private pauseAll() {
    // Pause all subscriptions
    this.subscriptions.forEach(unsubscribe => {
      unsubscribe();
    });
  }
}

Memory Management

Prevent memory leaks with proper subscription management:

class SubscriptionManager {
  private subscriptions = new Set<() => void>();

  add(unsubscribe: () => void) {
    this.subscriptions.add(unsubscribe);
    return unsubscribe;
  }

  remove(unsubscribe: () => void) {
    this.subscriptions.delete(unsubscribe);
  }

  clear() {
    this.subscriptions.forEach(unsubscribe => unsubscribe());
    this.subscriptions.clear();
  }
}

// Usage in a component
class UserComponent {
  private subscriptions = new SubscriptionManager();

  async initialize() {
    // Add subscriptions to manager
    const userSub = this.subscriptions.add(
      User.onList(user => this.handleUserUpdate(user))
    );

    const postSub = this.subscriptions.add(
      Post.onList(post => this.handlePostUpdate(post))
    );
  }

  destroy() {
    // Clean up all subscriptions
    this.subscriptions.clear();
  }
}

Performance Optimization

Limiting Real-time Data

Only subscribe to data you actually need:

// Only listen to recent posts instead of all posts
const unsubscribe = Post.query()
  .where('createdAt', '>', getLastWeek())
  .limit(50)
  .onSnapshot((posts) => {
    updateRecentPosts(posts);
  });

// Only listen to active users
const activeUsersUnsub = User.query()
  .where('isActive', '==', true)
  .onSnapshot((users) => {
    updateActiveUsersList(users);
  });

Conditional Subscriptions

Only set up real-time listening when needed:

class ConditionalRealtimeService {
  private activeSubscriptions = new Map<string, () => void>();

  startListening(key: string, condition: boolean) {
    if (condition && !this.activeSubscriptions.has(key)) {
      const unsubscribe = this.setupSubscription(key);
      this.activeSubscriptions.set(key, unsubscribe);
    } else if (!condition && this.activeSubscriptions.has(key)) {
      this.stopListening(key);
    }
  }

  stopListening(key: string) {
    const unsubscribe = this.activeSubscriptions.get(key);
    if (unsubscribe) {
      unsubscribe();
      this.activeSubscriptions.delete(key);
    }
  }

  private setupSubscription(key: string): () => void {
    switch (key) {
      case 'users':
        return User.onList(user => this.handleUserUpdate(user));
      case 'posts':
        return Post.onList(post => this.handlePostUpdate(post));
      default:
        return () => {};
    }
  }
}

// Usage based on user interaction
const service = new ConditionalRealtimeService();

// Start listening when user opens users page
service.startListening('users', userIsOnUsersPage);

// Stop listening when user navigates away
service.startListening('users', false);

Error Handling

Handle real-time listener errors gracefully:

function setupRealtimeWithErrorHandling() {
  const unsubscribe = User.onList(
    (user) => {
      try {
        updateUserInUI(user);
      } catch (error) {
        console.error('Error updating UI for user:', error);
        // Handle UI update error gracefully
      }
    },
    'modified'
  );

  // Wrap unsubscribe to handle errors
  return () => {
    try {
      unsubscribe();
    } catch (error) {
      console.error('Error unsubscribing from real-time listener:', error);
    }
  };
}

// Enhanced error handling with retry
class RobustRealtimeService {
  private maxRetries = 3;
  private retryDelay = 1000;

  async setupWithRetry(setupFn: () => (() => void), retries = 0): Promise<() => void> {
    try {
      return setupFn();
    } catch (error) {
      console.error(`Real-time setup failed (attempt ${retries + 1}):`, error);
      
      if (retries < this.maxRetries) {
        await new Promise(resolve => 
          setTimeout(resolve, this.retryDelay * Math.pow(2, retries))
        );
        return this.setupWithRetry(setupFn, retries + 1);
      } else {
        throw new Error('Real-time setup failed after maximum retries');
      }
    }
  }
}

Best Practices

1. Always Clean Up Subscriptions

// In React
useEffect(() => {
  const unsubscribe = User.onList(handleUserUpdate);
  return () => unsubscribe(); // Cleanup on unmount
}, []);

// In Vue
onUnmounted(() => {
  if (unsubscribe) {
    unsubscribe();
  }
});

// In Angular
ngOnDestroy() {
  if (this.unsubscribe) {
    this.unsubscribe();
  }
}

2. Use Specific Event Types

// Better: Only listen to the events you care about
const unsubscribe = User.onList(handleNewUser, 'added');

// Instead of: Listening to all events and filtering
const unsubscribe = User.onList((user) => {
  // Determine event type and handle accordingly
}, 'all');

3. Optimize Query Listeners

// Good: Specific, limited query
Post.query()
  .where('isPublished', '==', true)
  .orderBy('publishedAt', 'desc')
  .limit(20)
  .onSnapshot(handlePosts);

// Avoid: Listening to all documents
Post.onList(handleAllPosts); // Could be too much data

4. Handle Offline Scenarios

// Check online status before setting up listeners
if (navigator.onLine) {
  setupRealtimeListeners();
} else {
  // Load from cache or show offline message
  loadCachedData();
}

Common Patterns

Real-time Dashboard

class RealtimeDashboard {
  private subscriptions: (() => void)[] = [];

  async initialize() {
    // Real-time user count
    this.subscriptions.push(
      User.onModeList({
        added: () => this.updateUserCount(),
        removed: () => this.updateUserCount()
      })
    );

    // Real-time active sessions
    this.subscriptions.push(
      Session.query()
        .where('isActive', '==', true)
        .onSnapshot((sessions) => {
          this.updateActiveSessionsDisplay(sessions);
        })
    );

    // Real-time revenue tracking
    this.subscriptions.push(
      Order.query()
        .where('status', '==', 'completed')
        .where('createdAt', '>', getTodayStart())
        .onSnapshot((orders) => {
          this.updateRevenueDisplay(orders);
        })
    );
  }

  destroy() {
    this.subscriptions.forEach(unsubscribe => unsubscribe());
  }
}

Chat Application

class RealtimeChat {
  private messageSubscription?: () => void;

  async joinChannel(channelId: string) {
    // Leave previous channel
    this.leaveChannel();

    // Join new channel
    this.messageSubscription = Message.query()
      .where('channelId', '==', channelId)
      .orderBy('createdAt', 'desc')
      .limit(50)
      .onSnapshot((messages) => {
        this.displayMessages(messages.reverse());
      });
  }

  leaveChannel() {
    if (this.messageSubscription) {
      this.messageSubscription();
      this.messageSubscription = undefined;
    }
  }

  async sendMessage(text: string, channelId: string) {
    const message = new Message();
    message.text = text;
    message.channelId = channelId;
    message.authorId = getCurrentUserId();
    message.createdAt = new Date().toISOString();
    
    await message.save();
    // Real-time listener will automatically update UI
  }
}

Troubleshooting

Common Issues

Issue: Real-time listeners stop working Solution: Check internet connection and re-establish listeners if needed

Issue: Too many re-renders in React Solution: Use useCallback and useMemo to prevent unnecessary listener recreation

Issue: Memory leaks Solution: Always call unsubscribe functions in cleanup/unmount handlers

Issue: Listeners not triggering Solution: Verify Firestore security rules allow read access for the user

Debugging Real-time Issues

// Add logging to understand what's happening
const unsubscribe = User.onModeList({
  added: (user) => {
    console.log('🟢 User added:', user.getId(), user.name);
  },
  modified: (user) => {
    console.log('🟡 User modified:', user.getId(), user.name);
  },
  removed: (user) => {
    console.log('🔴 User removed:', user.getId());
  },
  init: (user) => {
    console.log('🔵 Initial user loaded:', user.getId(), user.name);
  }
});

Next Steps