Deep Analysis and Best Practices for Updating Arrays of Objects in Firestore

Nov 26, 2025 · Programming · 9 views · 7.8

Keywords: Firestore | Array Update | Object Arrays | NoSQL | JavaScript

Abstract: This article provides an in-depth exploration of the technical challenges and solutions for updating arrays of objects in Google Cloud Firestore. By analyzing the limitations of traditional methods, it details the usage of native array operations such as arrayUnion and arrayRemove, and compares the advantages and disadvantages of setting complete arrays versus using subcollections. With comprehensive code examples in JavaScript, the article offers a complete practical guide for implementing array CRUD operations, helping developers avoid common pitfalls and improve data manipulation efficiency.

Introduction

Google Cloud Firestore, as a NoSQL document database within the Firebase ecosystem, plays a crucial role in modern web and mobile application development. However, when it comes to updating complex data structures like arrays of objects, many developers encounter unexpected challenges. This article starts from practical problems to deeply analyze the core mechanisms of array updates in Firestore.

Limitations of Traditional Update Methods

In earlier versions of Firestore, developers typically attempted to update arrays using the following two approaches:

// Method 1: Using set operation
firebase.firestore()
  .collection('proprietary')
  .doc(docID)
  .set(
    { sharedWith: [{ who: "third@test.com", when: new Date() }] },
    { merge: true }
  )

// Method 2: Using update operation  
firebase.firestore()
  .collection('proprietary')
  .doc(docID)
  .update({ sharedWith: [{ who: "third@test.com", when: new Date() }] })

Both methods lead to the same outcome: complete overwriting of the existing array content. The fundamental reason is that Firestore treats the entire array as a single field value, and any update operation replaces the complete content of that field.

Native Array Operation Functions

Firestore now provides specialized array operation functions that allow developers to manage elements precisely without rewriting the entire array.

arrayUnion Function

The arrayUnion() function is used to add new elements to an array, adding only elements that are not already present:

const db = firebase.firestore();
const docRef = db.collection('proprietary').doc(docID);

const newSharedUser = {
  who: "third@test.com",
  when: firebase.firestore.FieldValue.serverTimestamp()
};

docRef.update({
  sharedWith: firebase.firestore.FieldValue.arrayUnion(newSharedUser)
}).then(() => {
  console.log("User successfully added to sharing list");
}).catch((error) => {
  console.error("Error updating document:", error);
});

It's important to note that arrayUnion() uses deep object comparison to determine if an element already exists. A new object is considered duplicate and ignored for addition only when it completely matches an existing object in the array.

arrayRemove Function

The arrayRemove() function is used to remove specified elements from an array:

const userToRemove = {
  who: "first@test.com",
  when: timestamp // Must exactly match the stored timestamp
};

docRef.update({
  sharedWith: firebase.firestore.FieldValue.arrayRemove(userToRemove)
}).then(() => {
  console.log("User removed from sharing list");
}).catch((error) => {
  console.error("Error removing user:", error);
});

Alternative Solutions Analysis

Solution 1: Setting Complete Array

When complex array operations are needed, a read-modify-write pattern can be adopted:

async function updateSharedWithArray(docID, newUser) {
  const db = firebase.firestore();
  const docRef = db.collection('proprietary').doc(docID);
  
  try {
    const doc = await docRef.get();
    if (!doc.exists) {
      throw new Error("Document does not exist");
    }
    
    const data = doc.data();
    const sharedWith = data.sharedWith || [];
    
    // Add new user
    sharedWith.push({
      who: newUser.email,
      when: firebase.firestore.FieldValue.serverTimestamp()
    });
    
    // Write back updated array
    await docRef.update({ sharedWith: sharedWith });
    console.log("Array updated successfully");
  } catch (error) {
    console.error("Error during update process:", error);
  }
}

This approach may face data race conditions in concurrent environments, and it's recommended to execute within a transaction to ensure data consistency.

Solution 2: Using Subcollections

Converting arrays to subcollections can avoid the limitations of array operations:

// Add shared user
firebase.firestore()
  .collection('proprietary')
  .doc(docID)
  .collection('sharedWith')
  .add({
    who: "third@test.com",
    when: firebase.firestore.FieldValue.serverTimestamp()
  });

// Query shared users
firebase.firestore()
  .collection('proprietary')
  .doc(docID)
  .collection('sharedWith')
  .orderBy('when', 'desc')
  .get()
  .then((querySnapshot) => {
    querySnapshot.forEach((doc) => {
      console.log(doc.id, " => ", doc.data());
    });
  });

The subcollection approach offers advantages including better query flexibility and independent object management, but requires additional read operations to retrieve complete data.

Performance and Best Practices

Data Modeling Considerations

When choosing between arrays and subcollections, consider the following factors:

Error Handling Strategies

Implementing robust array operations requires comprehensive error handling:

async function safeArrayUpdate(docRef, updateData) {
  try {
    await docRef.update(updateData);
    return { success: true };
  } catch (error) {
    if (error.code === 'not-found') {
      console.warn("Document does not exist, attempting to create new document");
      await docRef.set(updateData);
      return { success: true, created: true };
    }
    console.error("Update failed:", error);
    return { success: false, error: error };
  }
}

Practical Application Scenarios

User Permission Management System

When building multi-user collaborative applications, arrays can be used to manage document sharing permissions:

class DocumentSharingManager {
  constructor(db, collectionName) {
    this.db = db;
    this.collectionName = collectionName;
  }
  
  async shareDocument(docID, userEmail) {
    const docRef = this.db.collection(this.collectionName).doc(docID);
    const shareEntry = {
      who: userEmail,
      when: this.db.FieldValue.serverTimestamp(),
      permissions: ['read']
    };
    
    return await docRef.update({
      sharedWith: this.db.FieldValue.arrayUnion(shareEntry)
    });
  }
  
  async revokeAccess(docID, userEmail) {
    const docRef = this.db.collection(this.collectionName).doc(docID);
    
    // Need to read document first to find complete object to remove
    const doc = await docRef.get();
    if (!doc.exists) return;
    
    const sharedWith = doc.data().sharedWith || [];
    const userEntry = sharedWith.find(entry => entry.who === userEmail);
    
    if (userEntry) {
      return await docRef.update({
        sharedWith: this.db.FieldValue.arrayRemove(userEntry)
      });
    }
  }
}

Conclusion

Array update operations in Firestore require developers to deeply understand its data model and behavioral characteristics. Native functions arrayUnion and arrayRemove provide efficient array element management capabilities, but in complex scenarios, setting complete arrays or using subcollections may be more appropriate choices. Developers should select the most suitable solution based on specific application requirements, data scale, and performance needs, while paying attention to handling concurrent updates and data consistency issues.

Copyright Notice: All rights in this article are reserved by the operators of DevGex. Reasonable sharing and citation are welcome; any reproduction, excerpting, or re-publication without prior permission is prohibited.