Skip to main content
RapidDev - Software Development Agency
firebase-tutorial

How to Unsubscribe from a Firestore Snapshot Listener

To unsubscribe from a Firestore snapshot listener, store the return value of onSnapshot() in a variable and call it as a function when you no longer need updates. In React, return the unsubscribe function from a useEffect cleanup. Failing to unsubscribe causes memory leaks, stale UI updates, and unnecessary Firestore read charges that accumulate over time.

What you'll learn

  • How onSnapshot() returns an unsubscribe function and how to call it
  • How to properly clean up listeners in React useEffect hooks
  • How to manage multiple listeners and unsubscribe from all of them
  • How to avoid memory leaks and unnecessary Firestore read costs
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner7 min read10-12 minFirebase JS SDK v9+, React 18+, all Firebase plansMarch 2026RapidDev Engineering Team
TL;DR

To unsubscribe from a Firestore snapshot listener, store the return value of onSnapshot() in a variable and call it as a function when you no longer need updates. In React, return the unsubscribe function from a useEffect cleanup. Failing to unsubscribe causes memory leaks, stale UI updates, and unnecessary Firestore read charges that accumulate over time.

Unsubscribing from Firestore Snapshot Listeners

Firestore's onSnapshot() creates a persistent connection that streams document changes to your app in real time. Every listener keeps a WebSocket open and counts reads against your Firestore quota. If you navigate away from a page or unmount a component without unsubscribing, the listener stays active in the background, wasting resources and potentially causing errors when it tries to update unmounted components. This tutorial shows you how to properly unsubscribe in plain JavaScript and in React.

Prerequisites

  • A Firebase project with Firestore enabled
  • Firebase JS SDK v9+ installed in your project
  • Basic understanding of JavaScript Promises and closures
  • Familiarity with React hooks if using the React examples

Step-by-step guide

1

Understand the onSnapshot return value

When you call onSnapshot() on a document or query reference, it returns an Unsubscribe function. This function takes no arguments and stops the listener when called. The key insight is that onSnapshot() does not return a Promise or data — it returns the cleanup function directly.

typescript
1import { doc, onSnapshot } from 'firebase/firestore';
2
3// onSnapshot returns an unsubscribe function
4const unsubscribe = onSnapshot(
5 doc(db, 'users', 'user123'),
6 (snapshot) => {
7 console.log('Current data:', snapshot.data());
8 }
9);
10
11// Later, when you no longer need updates:
12unsubscribe();

Expected result: The listener is active and streaming updates until unsubscribe() is called.

2

Unsubscribe in a React useEffect cleanup

In React, the standard pattern is to start the listener inside useEffect and return the unsubscribe function as the cleanup. React calls the cleanup function when the component unmounts or when the dependency array changes. This prevents memory leaks and avoids the 'Can't perform a React state update on an unmounted component' warning.

typescript
1import { useEffect, useState } from 'react';
2import { doc, onSnapshot } from 'firebase/firestore';
3import { db } from './firebase';
4
5interface UserProfile {
6 name: string;
7 email: string;
8}
9
10export function useUserProfile(userId: string) {
11 const [profile, setProfile] = useState<UserProfile | null>(null);
12 const [loading, setLoading] = useState(true);
13
14 useEffect(() => {
15 const unsubscribe = onSnapshot(
16 doc(db, 'users', userId),
17 (snapshot) => {
18 setProfile(snapshot.exists() ? (snapshot.data() as UserProfile) : null);
19 setLoading(false);
20 },
21 (error) => {
22 console.error('Listener error:', error);
23 setLoading(false);
24 }
25 );
26
27 // React calls this when component unmounts or userId changes
28 return unsubscribe;
29 }, [userId]);
30
31 return { profile, loading };
32}

Expected result: The listener starts when the component mounts and stops automatically when it unmounts or when userId changes.

3

Unsubscribe from a query listener

Query listeners work the same way as document listeners. Call onSnapshot() on a Query object and store the returned function. The snapshot in the callback is a QuerySnapshot with a docs array. Each re-render of the query results counts as reads for every document in the result set, so unsubscribing promptly is especially important for large collections.

typescript
1import { useEffect, useState } from 'react';
2import { collection, query, where, orderBy, onSnapshot } from 'firebase/firestore';
3import { db } from './firebase';
4
5interface Task {
6 id: string;
7 title: string;
8 completed: boolean;
9}
10
11export function useTasks(userId: string) {
12 const [tasks, setTasks] = useState<Task[]>([]);
13
14 useEffect(() => {
15 const q = query(
16 collection(db, 'tasks'),
17 where('ownerId', '==', userId),
18 orderBy('createdAt', 'desc')
19 );
20
21 const unsubscribe = onSnapshot(q, (snapshot) => {
22 const items = snapshot.docs.map((doc) => ({
23 id: doc.id,
24 ...doc.data(),
25 })) as Task[];
26 setTasks(items);
27 });
28
29 return unsubscribe;
30 }, [userId]);
31
32 return tasks;
33}

Expected result: The query listener streams task updates in real time and stops when the component unmounts.

4

Manage multiple listeners with a cleanup array

When a component subscribes to several Firestore paths, track all unsubscribe functions in an array and call them all during cleanup. This pattern prevents partial cleanup where some listeners keep running after the component unmounts.

typescript
1import { useEffect } from 'react';
2import { doc, collection, onSnapshot } from 'firebase/firestore';
3import { db } from './firebase';
4
5export function useDashboard(userId: string) {
6 useEffect(() => {
7 const unsubscribes: (() => void)[] = [];
8
9 // Listener 1: user profile
10 unsubscribes.push(
11 onSnapshot(doc(db, 'users', userId), (snap) => {
12 // handle profile update
13 })
14 );
15
16 // Listener 2: notifications
17 unsubscribes.push(
18 onSnapshot(collection(db, 'users', userId, 'notifications'), (snap) => {
19 // handle notifications update
20 })
21 );
22
23 // Cleanup: unsubscribe from all listeners
24 return () => {
25 unsubscribes.forEach((unsub) => unsub());
26 };
27 }, [userId]);
28}

Expected result: All listeners are properly cleaned up when the component unmounts, preventing memory leaks.

5

Handle errors in the listener callback

The third argument to onSnapshot() is an error callback. If a security rule changes or the user loses authentication while a listener is active, Firestore triggers this callback. Always handle errors to prevent silent failures and to clean up the listener gracefully.

typescript
1import { doc, onSnapshot, FirestoreError } from 'firebase/firestore';
2
3const unsubscribe = onSnapshot(
4 doc(db, 'orders', 'order123'),
5 (snapshot) => {
6 console.log('Order data:', snapshot.data());
7 },
8 (error: FirestoreError) => {
9 console.error('Listener failed:', error.code, error.message);
10 // Common: 'permission-denied' when auth state changes
11 if (error.code === 'permission-denied') {
12 unsubscribe(); // Stop the broken listener
13 }
14 }
15);

Expected result: Errors are caught and logged instead of failing silently, and broken listeners are cleaned up.

Complete working example

useFirestoreListener.ts
1import { useEffect, useState } from 'react';
2import {
3 doc,
4 collection,
5 query,
6 where,
7 orderBy,
8 onSnapshot,
9 DocumentData,
10 QueryConstraint,
11} from 'firebase/firestore';
12import { db } from './firebase';
13
14// Generic hook for a single document listener
15export function useDocument<T = DocumentData>(
16 path: string,
17 id: string
18) {
19 const [data, setData] = useState<T | null>(null);
20 const [loading, setLoading] = useState(true);
21 const [error, setError] = useState<string | null>(null);
22
23 useEffect(() => {
24 setLoading(true);
25 const unsubscribe = onSnapshot(
26 doc(db, path, id),
27 (snapshot) => {
28 setData(snapshot.exists() ? (snapshot.data() as T) : null);
29 setLoading(false);
30 setError(null);
31 },
32 (err) => {
33 setError(err.message);
34 setLoading(false);
35 }
36 );
37 return unsubscribe;
38 }, [path, id]);
39
40 return { data, loading, error };
41}
42
43// Generic hook for a collection query listener
44export function useCollection<T = DocumentData>(
45 path: string,
46 ...constraints: QueryConstraint[]
47) {
48 const [data, setData] = useState<(T & { id: string })[]>([]);
49 const [loading, setLoading] = useState(true);
50
51 useEffect(() => {
52 const q = query(collection(db, path), ...constraints);
53 const unsubscribe = onSnapshot(q, (snapshot) => {
54 const items = snapshot.docs.map((d) => ({
55 id: d.id,
56 ...(d.data() as T),
57 }));
58 setData(items);
59 setLoading(false);
60 });
61 return unsubscribe;
62 }, [path, ...constraints]);
63
64 return { data, loading };
65}

Common mistakes when unsubscribing from a Firestore Snapshot Listener

Why it's a problem: Not storing the onSnapshot return value, making it impossible to unsubscribe

How to avoid: Always assign the return value to a variable: const unsubscribe = onSnapshot(...). Without it, the listener runs forever.

Why it's a problem: Calling onSnapshot inside useEffect without returning the unsubscribe function

How to avoid: Return the unsubscribe function directly from useEffect: return unsubscribe; or return () => unsubscribe();

Why it's a problem: Creating a new listener on every render by placing onSnapshot outside useEffect

How to avoid: Always place onSnapshot inside useEffect with proper dependencies. Each call creates a new WebSocket connection.

Best practices

  • Always return the unsubscribe function from React useEffect to prevent memory leaks on unmount
  • Include all relevant dependencies (userId, documentId) in the useEffect dependency array
  • Use the error callback in onSnapshot to handle permission changes and network failures gracefully
  • Track multiple unsubscribe functions in an array when a component has several listeners
  • Create reusable custom hooks (useDocument, useCollection) to standardize listener management
  • Avoid listening to large collections without query constraints — each document counts as a read on every update
  • Unsubscribe from listeners when the user signs out to prevent permission-denied errors

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

Show me how to properly unsubscribe from Firestore onSnapshot listeners in a React app using Firebase v9 modular SDK. Include a custom useEffect hook that handles cleanup, error handling, and TypeScript types.

Firebase Prompt

Create a reusable React hook that subscribes to a Firestore document with onSnapshot and properly unsubscribes on cleanup. Use Firebase modular SDK v9+ imports, TypeScript generics, and include error state handling.

Frequently asked questions

What happens if I forget to unsubscribe from a Firestore listener?

The listener continues running in the background, consuming memory, using bandwidth, and counting Firestore reads against your quota. In React, it can also cause errors when trying to update state on unmounted components.

Does unsubscribing from a listener cancel in-flight reads?

Calling unsubscribe stops future updates but does not cancel the current snapshot being processed. Any reads already charged are not refunded.

Can I resubscribe after calling unsubscribe?

You cannot reuse the same unsubscribe function. To listen again, call onSnapshot() again to create a new listener and get a new unsubscribe function.

Do Firestore listeners automatically stop when the browser tab closes?

Yes. When the browser tab or window closes, all WebSocket connections are terminated and listeners stop. However, navigating between pages in an SPA does not close the tab, so you must unsubscribe manually.

How many active listeners can I have at once?

There is no hard limit from Firebase, but each listener uses a WebSocket connection and memory. In practice, keep active listeners under 100 per client to avoid performance issues.

Can RapidDev help optimize my real-time Firestore architecture?

Yes. RapidDev can audit your Firestore listener patterns, reduce unnecessary reads, and implement efficient real-time data flows that scale without excessive costs.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.