To filter Firestore documents by array field values, use the where() function with the array-contains operator. This returns all documents where the specified array field contains the given value. For matching any of multiple values, use array-contains-any with an array of up to 10 values. You can combine array-contains with equality filters in compound queries, but you cannot use array-contains and array-contains-any in the same query, and you can only have one array-contains clause per query.
Filtering Firestore Documents by Array Fields
Firestore provides two array-specific query operators: array-contains for checking if an array field includes a single value, and array-contains-any for matching against multiple possible values. These operators are powerful for tags, categories, roles, and other multi-value fields. This tutorial covers both operators, their limitations, and data modeling strategies that make array queries efficient.
Prerequisites
- A Firebase project with Firestore enabled
- Firebase JS SDK v9+ installed in your project
- Firestore initialized with initializeApp and getFirestore
- Documents with array fields to query against
Step-by-step guide
Query with array-contains for a single value
Query with array-contains for a single value
The array-contains operator checks whether an array field contains a specific value. It works with strings, numbers, and booleans inside the array. Import query, collection, where, and getDocs from firebase/firestore. Build the query with where('fieldName', 'array-contains', value) and execute it with getDocs. Firestore automatically indexes array fields for array-contains queries — no composite index is needed for single-operator queries.
1import { getFirestore, collection, query, where, getDocs } from 'firebase/firestore';23const db = getFirestore();45// Find all articles tagged with 'firebase'6async function getArticlesByTag(tag: string) {7 const q = query(8 collection(db, 'articles'),9 where('tags', 'array-contains', tag)10 );1112 const snapshot = await getDocs(q);13 return snapshot.docs.map((doc) => ({14 id: doc.id,15 ...doc.data()16 }));17}1819// Example: all articles with the 'firebase' tag20const articles = await getArticlesByTag('firebase');Expected result: Returns all documents where the tags array field includes the exact value 'firebase'.
Query with array-contains-any for multiple values
Query with array-contains-any for multiple values
The array-contains-any operator returns documents where the array field contains at least one of the provided values. Pass an array of up to 10 values. This is useful for filtering by multiple tags or categories in a single query. If you need more than 10 values, split them into multiple queries and merge the results client-side.
1import { getFirestore, collection, query, where, getDocs } from 'firebase/firestore';23const db = getFirestore();45// Find articles tagged with ANY of these topics6async function getArticlesByAnyTag(tags: string[]) {7 const q = query(8 collection(db, 'articles'),9 where('tags', 'array-contains-any', tags)10 );1112 const snapshot = await getDocs(q);13 return snapshot.docs.map((doc) => ({14 id: doc.id,15 ...doc.data()16 }));17}1819// Returns articles tagged with 'firebase' OR 'react' OR 'typescript'20const articles = await getArticlesByAnyTag(['firebase', 'react', 'typescript']);Expected result: Returns all documents where the tags array contains at least one of the specified values.
Combine array-contains with other where clauses
Combine array-contains with other where clauses
You can combine array-contains with equality (==) and comparison (<, >, etc.) operators in the same query. However, compound queries with range operators and array-contains require a composite index. Firestore provides a direct link in the error message to create the required index when you first run the query.
1import { getFirestore, collection, query, where, orderBy, limit, getDocs } from 'firebase/firestore';23const db = getFirestore();45// Find published articles with the 'firebase' tag, ordered by date6async function getPublishedArticlesByTag(tag: string) {7 const q = query(8 collection(db, 'articles'),9 where('tags', 'array-contains', tag),10 where('status', '==', 'published'),11 orderBy('publishedAt', 'desc'),12 limit(20)13 );1415 const snapshot = await getDocs(q);16 return snapshot.docs.map((doc) => ({17 id: doc.id,18 ...doc.data()19 }));20}Expected result: Returns up to 20 published articles with the specified tag, ordered by publish date descending.
Understand the limitations of array queries
Understand the limitations of array queries
Firestore enforces strict rules on array operators: (1) You can only use one array-contains or array-contains-any per query. (2) You cannot combine array-contains-any with in or not-in in the same query. (3) array-contains matches the exact element — it cannot match partial strings or nested object fields within the array. (4) If you need to filter by ALL values (AND logic), array-contains only supports one value per query. For AND logic across multiple values, run separate queries and intersect the results, or restructure your data model.
1// These will NOT work:23// ERROR: Cannot have two array-contains in one query4const bad1 = query(5 collection(db, 'articles'),6 where('tags', 'array-contains', 'firebase'),7 where('tags', 'array-contains', 'react')8);910// ERROR: Cannot combine array-contains-any with in11const bad2 = query(12 collection(db, 'articles'),13 where('tags', 'array-contains-any', ['firebase', 'react']),14 where('status', 'in', ['published', 'draft'])15);1617// WORKAROUND for AND logic: use a map field instead18// Store { firebase: true, react: true } and query with:19const workaround = query(20 collection(db, 'articles'),21 where('tagMap.firebase', '==', true),22 where('tagMap.react', '==', true)23);Expected result: You understand which query combinations are allowed and can choose the right data model for your filtering needs.
Use array-contains with real-time listeners
Use array-contains with real-time listeners
Array-contains queries work with onSnapshot for real-time updates just like any other Firestore query. When a document's array field changes (a tag is added or removed), the listener fires with the updated results. Use the same query construction and pass it to onSnapshot instead of getDocs.
1import { getFirestore, collection, query, where, onSnapshot } from 'firebase/firestore';23const db = getFirestore();45function listenToTaggedArticles(tag: string, callback: (articles: any[]) => void) {6 const q = query(7 collection(db, 'articles'),8 where('tags', 'array-contains', tag)9 );1011 const unsubscribe = onSnapshot(q, (snapshot) => {12 const articles = snapshot.docs.map((doc) => ({13 id: doc.id,14 ...doc.data()15 }));16 callback(articles);17 });1819 return unsubscribe;20}2122// Start listening23const unsub = listenToTaggedArticles('firebase', (articles) => {24 console.log('Updated articles:', articles.length);25});2627// Call unsub() to stop listeningExpected result: The callback fires initially with matching documents and again whenever a matching document is added, modified, or removed.
Complete working example
1import { initializeApp } from 'firebase/app';2import {3 getFirestore,4 collection,5 query,6 where,7 orderBy,8 limit,9 getDocs,10 onSnapshot,11 Unsubscribe12} from 'firebase/firestore';1314const app = initializeApp({15 // Your Firebase config16});17const db = getFirestore(app);1819interface Article {20 id: string;21 title: string;22 tags: string[];23 status: string;24 publishedAt: Date;25}2627// Find documents containing a specific tag28export async function getByTag(tag: string): Promise<Article[]> {29 const q = query(30 collection(db, 'articles'),31 where('tags', 'array-contains', tag)32 );33 const snap = await getDocs(q);34 return snap.docs.map((d) => ({ id: d.id, ...d.data() } as Article));35}3637// Find documents containing any of the given tags38export async function getByAnyTag(tags: string[]): Promise<Article[]> {39 const q = query(40 collection(db, 'articles'),41 where('tags', 'array-contains-any', tags.slice(0, 10))42 );43 const snap = await getDocs(q);44 return snap.docs.map((d) => ({ id: d.id, ...d.data() } as Article));45}4647// Compound query: tag + status filter + ordering48export async function getPublishedByTag(49 tag: string,50 max: number = 2051): Promise<Article[]> {52 const q = query(53 collection(db, 'articles'),54 where('tags', 'array-contains', tag),55 where('status', '==', 'published'),56 orderBy('publishedAt', 'desc'),57 limit(max)58 );59 const snap = await getDocs(q);60 return snap.docs.map((d) => ({ id: d.id, ...d.data() } as Article));61}6263// Real-time listener for tagged articles64export function onTaggedArticles(65 tag: string,66 callback: (articles: Article[]) => void67): Unsubscribe {68 const q = query(69 collection(db, 'articles'),70 where('tags', 'array-contains', tag)71 );72 return onSnapshot(q, (snap) => {73 callback(snap.docs.map((d) => ({ id: d.id, ...d.data() } as Article)));74 });75}Common mistakes when filtering Documents by Array Contains in Firestore
Why it's a problem: Using two array-contains clauses in the same query for AND logic
How to avoid: Firestore allows only one array-contains per query. For AND logic, use a map field instead (e.g., tagMap: { firebase: true, react: true }) and query with multiple equality filters.
Why it's a problem: Passing more than 10 values to array-contains-any
How to avoid: The limit is 10 values per query. Split larger sets into batches of 10, run separate queries, and merge results while deduplicating by document ID.
Why it's a problem: Expecting array-contains to match partial strings or substrings
How to avoid: array-contains performs exact element matching. Store values in a consistent format. For substring search, consider maintaining a searchable keywords array or using an external search service.
Why it's a problem: Not creating composite indexes for compound queries with array-contains
How to avoid: When combining array-contains with orderBy or range filters, Firestore requires a composite index. Click the error link in the console to create it, or add it to firestore.indexes.json.
Best practices
- Store array values in a consistent format (lowercase strings) for reliable array-contains matching
- Use array-contains-any instead of multiple queries when you need OR logic across tags
- Limit arrays to a reasonable size — very large arrays (1000+ elements) increase document read costs
- Consider a map field instead of an array when you need AND logic across multiple values
- Create composite indexes proactively in firestore.indexes.json for known compound queries
- Use arrayUnion and arrayRemove for atomic array modifications instead of reading and rewriting the entire array
- Combine array-contains with pagination (limit + startAfter) for large result sets
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
Show me how to filter Firestore documents using array-contains and array-contains-any in the modular SDK v9+. Include examples for single tag filtering, multi-tag OR filtering, compound queries with other where clauses, and a real-time listener. Explain the query limitations and data modeling alternatives for AND logic.
Write TypeScript utility functions for Firestore array queries: getByTag using array-contains, getByAnyTag using array-contains-any (handling the 10-value limit), a compound query combining array-contains with equality filter and orderBy, and a real-time listener. Include the Article interface and proper typing.
Frequently asked questions
Can I use array-contains to check for objects inside an array?
Yes, but the object must match exactly — all fields and values must be identical. You cannot match partial objects or query individual fields within array objects. For complex array element queries, consider restructuring as a subcollection.
How many elements can a Firestore array field contain?
There is no explicit array element limit, but the entire document must be under 1 MiB and 20,000 fields. In practice, keep arrays under a few hundred elements for performance. Very large arrays increase read costs and index size.
Can I do a NOT array-contains query?
No. Firestore does not support a negated array-contains operator. As a workaround, query without the array filter and filter out unwanted results client-side, or restructure your data to use a different querying strategy.
Does array-contains work with the Firestore Emulator?
Yes. The Firestore Emulator fully supports array-contains and array-contains-any queries with the same behavior as production, including composite index requirements.
What is the difference between array-contains and in?
array-contains checks if a document's array field includes a specific value. 'in' checks if a document's scalar field matches any value in a provided list. They operate on different field types.
Can RapidDev help design Firestore data models for complex filtering requirements?
Yes, RapidDev can analyze your query patterns and design optimal Firestore data models using arrays, maps, subcollections, and denormalization to support efficient filtering without excessive read costs.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation