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
Understand the onSnapshot return value
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.
1import { doc, onSnapshot } from 'firebase/firestore';23// onSnapshot returns an unsubscribe function4const unsubscribe = onSnapshot(5 doc(db, 'users', 'user123'),6 (snapshot) => {7 console.log('Current data:', snapshot.data());8 }9);1011// Later, when you no longer need updates:12unsubscribe();Expected result: The listener is active and streaming updates until unsubscribe() is called.
Unsubscribe in a React useEffect cleanup
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.
1import { useEffect, useState } from 'react';2import { doc, onSnapshot } from 'firebase/firestore';3import { db } from './firebase';45interface UserProfile {6 name: string;7 email: string;8}910export function useUserProfile(userId: string) {11 const [profile, setProfile] = useState<UserProfile | null>(null);12 const [loading, setLoading] = useState(true);1314 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 );2627 // React calls this when component unmounts or userId changes28 return unsubscribe;29 }, [userId]);3031 return { profile, loading };32}Expected result: The listener starts when the component mounts and stops automatically when it unmounts or when userId changes.
Unsubscribe from a query listener
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.
1import { useEffect, useState } from 'react';2import { collection, query, where, orderBy, onSnapshot } from 'firebase/firestore';3import { db } from './firebase';45interface Task {6 id: string;7 title: string;8 completed: boolean;9}1011export function useTasks(userId: string) {12 const [tasks, setTasks] = useState<Task[]>([]);1314 useEffect(() => {15 const q = query(16 collection(db, 'tasks'),17 where('ownerId', '==', userId),18 orderBy('createdAt', 'desc')19 );2021 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 });2829 return unsubscribe;30 }, [userId]);3132 return tasks;33}Expected result: The query listener streams task updates in real time and stops when the component unmounts.
Manage multiple listeners with a cleanup array
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.
1import { useEffect } from 'react';2import { doc, collection, onSnapshot } from 'firebase/firestore';3import { db } from './firebase';45export function useDashboard(userId: string) {6 useEffect(() => {7 const unsubscribes: (() => void)[] = [];89 // Listener 1: user profile10 unsubscribes.push(11 onSnapshot(doc(db, 'users', userId), (snap) => {12 // handle profile update13 })14 );1516 // Listener 2: notifications17 unsubscribes.push(18 onSnapshot(collection(db, 'users', userId, 'notifications'), (snap) => {19 // handle notifications update20 })21 );2223 // Cleanup: unsubscribe from all listeners24 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.
Handle errors in the listener callback
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.
1import { doc, onSnapshot, FirestoreError } from 'firebase/firestore';23const 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 changes11 if (error.code === 'permission-denied') {12 unsubscribe(); // Stop the broken listener13 }14 }15);Expected result: Errors are caught and logged instead of failing silently, and broken listeners are cleaned up.
Complete working example
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';1314// Generic hook for a single document listener15export function useDocument<T = DocumentData>(16 path: string,17 id: string18) {19 const [data, setData] = useState<T | null>(null);20 const [loading, setLoading] = useState(true);21 const [error, setError] = useState<string | null>(null);2223 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]);3940 return { data, loading, error };41}4243// Generic hook for a collection query listener44export 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);5051 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]);6364 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.
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.
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation