简体   繁体   English

在实时 Firestore 中返回 onSnapshot 时如何提取数据?

[英]How to extract data when onSnapshot is returned in realtime firestore?

I have two files contact.js and functions.js .我有两个文件contact.jsfunctions.js I am using firestore realtime functionality.我正在使用 Firestore 实时功能。

Here is my functions.js file code:这是我的functions.js文件代码:

export const getUserContacts = () => {
  const contactDetailsArr = [];

  return db.collection("users").doc(userId)
    .onSnapshot(docs => {

      const contactsObject = docs.data().contacts;

      for (let contact in contactsObject) {

        db.collection("users").doc(contact).get()
          .then(userDetail => {
            contactDetailsArr.push({
              userId: contact,
              lastMessage: contactsObject[contact].lastMsg,
              time: contactsObject[contact].lastMsgTime,
              userName:userDetail.data().userName,
              email: userDetail.data().emailId,
              active: userDetail.data().active,
              img: userDetail.data().imageUrl,
              unreadMsg:contactsObject[contact].unreadMsg
            })

          })
      }
      console.log(contactDetailsArr);
      return contactDetailsArr;

    })
}

in contact.js when I do:当我这样做时在contact.js中:

useEffect(() => {
    let temp = getUserContacts();
    console.log(temp);
}, [])

I want to extract data of contactDetailsArr in contacts.js but I get the value of temp consoled as:我想在contacts.js中提取contactDetailsArr的数据,但我得到的 temp 值被控制台为:

ƒ () {
      i.Zl(), r.cs.ws(function () {
        return Pr(r.q_, o);
      });
    }

How do I extract the array data in my case?在我的情况下如何提取数组数据?

The onSnapshot() returns a function that can be used to detach the Firestore listener. onSnapshot()返回可用于分离 Firestore 侦听器的 function。 When using a listener, it's best to set the data directly into state rather than returning something from that function.使用监听器时,最好将数据直接设置到 state 中,而不是从 function 中返回内容。 Try refactoring the code as shown below:尝试重构代码,如下所示:

const [contacts, setContacts] = useState([]);

useEffect(() => {
  const getUserContacts = () => {
    const contactDetailsArr = [];

    const detach = db.collection("users").doc(userId)
      .onSnapshot(docs => {
        const contactsObject = docs.data().contacts;
        const contactsSnap = await Promise.all(contactsObject.map((c) => db.collection("users").doc(c).get()))

        const contactDetails = contactsSnap.map((d) => ({
          id: d.id,
          ...d.data()
          // other fields like unreadMsg, time
        }))

        // Update in state 
        setContacts(contactDetails);
      })
  }

  getUserContacts();
}, [])

Then use contacts array to map data in to UI directly.然后使用contacts数组将 map 数据直接输入到 UI。

Assumptions假设

This answer assumes a user's data looks like this in your Firestore:此答案假设用户的数据在您的 Firestore 中如下所示:

// Document at /users/someUserId
{
  "active": true,
  "contacts": {
    "someOtherUserId": {
      "lastMsg": "This is a message",
      "lastMsgTime": /* Timestamp */,
      "unreadMsg": true // unclear if this is a boolean or a count of messages
    },
    "anotherUserId": {
      "lastMsg": "Hi some user! How are you?",
      "lastMsgTime": /* Timestamp */,
      "unreadMsg": false
    }
  },
  "emailId": "someuser@example.com",
  "imageUrl": "https://firebasestorage.googleapis.com/b/bucket/o/images%20stars.jpg",
  "userName": "Some User"
}

Note: When asking questions in the future, please add examples of your data structure similar to the above注:日后提问时,请添加与上述类似的数据结构示例


Attaching Listeners with Current Structure使用当前结构附加侦听器

The structure as shown above has a number of flaws.如上所示的结构有许多缺陷。 The "contacts" object in the user's data should be moved to a sub-collection of the user's main document.用户数据中的“联系人”object 应移动到用户主文档的子集合中。 The reasons for this include:其原因包括:

  • Any user can read another user's (latest) messages (which can't be blocked with security rules)任何用户都可以阅读其他用户的(最新)消息(不能被安全规则阻止)
  • Any user can read another user's contacts list (which can't be blocked with security rules)任何用户都可以读取另一个用户的联系人列表(不能被安全规则阻止)
  • As an individual user messages more users, their user data will grow rapidly in size随着个人用户向更多用户发送消息,他们的用户数据将迅速增长
  • Each time you want to read a user's data, you have to download their entire message map even if not using it每次要读取用户的数据时,即使不使用,也必须下载他们的整个消息 map
  • As you fill out a user's contacts array, you are fetching their entire user data document even though you only need their active , email , imageUrl , and userName properties当您填写用户的联系人数组时,您正在获取他们的整个用户数据文档,即使您只需要他们的activeemailimageUrluserName属性
  • Higher chance of encountering document write conflicts when two users are editing the contact list of the same user (such as when sending a message)当两个用户在编辑同一用户的联系人列表时(例如发送消息时),遇到文档写入冲突的可能性更高
  • Hard to (efficiently) detect changes to a user's contact list (eg new addition, deletion)难以(有效)检测到用户联系人列表的更改(例如新增、删除)
  • Hard to (efficiently) listen to changes to another user's active status, email, profile image and display name as the listeners would be fired for every message update they receive很难(有效地)监听对另一个用户的活动状态、email、个人资料图像和显示名称的更改,因为监听器将在收到的每个消息更新时被触发

To fetch a user's contacts once in your functions.js library, you would use:要在您的functions.js库中获取用户的联系人一次,您可以使用:

// Utility function: Used to hydrate an entry in a user's "contacts" map
const getContactFromContactMapEntry = (db, [contactId, msgInfo]) => {
  return db.collection("users")
    .doc(contactId)
    .get()
    .then((contactDocSnapshot) => {
      const { lastMsg, lastMsgTime, unreadMsg, userName } = msgInfo;
      
      const baseContactData = {
        lastMessage: lastMsg,
        time: lastMsgTime,
        unreadMsg,
        userId: contactId
      }
      
      if (!contactDocSnapshot.exists) {
        // TODO: Decide how to handle unknown/deleted users
        return {
          ...baseContactData,
          active: false,           // deleted users are inactive, nor do they
          email: null,             // have an email, image or display name
          img: null,
          userName: "Deleted user"
        };
      }
      
      const { active, emailId, imageUrl, userName } = contactDocSnapshot.data();
      
      return {
        ...baseContactData,
        active,
        email: emailId,
        img: imageUrl,
        userName
      };
    });
};

export const getUserContacts = (db, userId) => { // <-- note that db and userId are passed in
  return db.collection("users")
    .doc(userId)
    .get()
    .then((userDataSnapshot) => {
      const contactsMetadataMap = userDataSnapshot.get("contacts");
      return Promise.all( // <-- waits for each Promise to complete
        Object.entries(contactsMetadataMap) // <-- used to get an array of id-value pairs that we can iterate over
          .map(getContactFromContactMapEntry.bind(null, db)); // for each contact, call the function (reusing db), returning a Promise with the data
      );
    });
}

Example Usage:示例用法:

getUserContacts(db, userId)
  .then((contacts) => console.log("Contacts data:", contacts))
  .catch((err) => console.error("Failed to get contacts:", err))
  
// OR

try {
  const contacts = await getUserContacts(db, userId);
  console.log("Contacts data:", contacts);
} catch (err) {
  console.error("Failed to get contacts:", err)
}

To fetch a user's contacts, and keep the list updated, using a function in your functions.js library, you would use:要获取用户的联系人并保持列表更新,请在您的functions.js库中使用 function,您可以使用:

// reuse getContactFromContactMapEntry as above

export const useUserContacts = (db, userId) => {
  if (!db) throw new TypeError("Parameter 'db' is required");
  
  const [userContactsData, setUserContactsData] = useState({ loading: true, contacts: [], error: null });
  
  useEffect(() => {
    // no user signed in?
    if (!userId) {
      setUserContactsData({ loading: false, contacts: [], error: "No user signed in" });
      return;
    }
    
    // update loading status (as needed)
    if (!userContactsData.loading) {
      setUserContactsData({ loading: true, contacts: [], error: null });
    }
    
    let detached = false;
    
    const detachListener = db.collection("users")
      .doc(userId)
      .onSnapshot({
        next: (userDataSnapshot) => {
          const contactsMetadataMap = userDataSnapshot.get("contacts");
          
          const hydrateContactsPromise = Promise.all( // <-- waits for each Promise to complete
            Object.entries(contactsMetadataMap) // <-- used to get an array of id-value pairs that we can iterate over
              .map(getContactFromContactMapEntry.bind(null, db)); // for each contact, call the function (reusing db), returning a Promise with the data
          );
          
          hydrateContactsPromise
            .then((contacts) => {
              if (detached) return; // detached already, do nothing.
              setUserContactsData({ loading: false, contacts, error: null });
            })
            .catch((err) => {
              if (detached) return; // detached already, do nothing.
              setUserContactsData({ loading: false, contacts: [], error: err });
            });
        },
        error: (err) => {
          setUserContactsData({ loading: false, contacts: [], error: err });
        }
      });
      
    return () => {
      detached = true;
      detachListener();
    }
  }, [db, userId])
}

Note: The above code will not (due to complexity):注意:上面的代码不会(由于复杂性):

  • react to changes in another user's active status, email or profile image对另一个用户的活动状态、email 或个人资料图像的变化做出反应
  • properly handle when the setUserContactsData method is called out of order due to network issues网络问题导致setUserContactsData方法被乱序调用时正确处理
  • handle when db instance is changed on every render在每次渲染时更改db实例时处理

Example Usage:示例用法:

const { loading, contacts, error } = useUserContacts(db, userId);

Attaching Listeners with Sub-collection Structure使用子集合结构附加侦听器

To restructure your data for efficiency, your structure would be updated to the following:为了提高效率重组您的数据,您的结构将更新为以下内容:

// Document at /users/someUserId
{
  "active": true,
  "emailId": "someuser@example.com",
  "imageUrl": "https://firebasestorage.googleapis.com/b/bucket/o/images%20stars.jpg",
  "userName": "Some User"
}

// Document at /users/someUserId/contacts/someOtherUserId
{
  "lastMsg": "This is a message",
  "lastMsgTime": /* Timestamp */,
  "unreadMsg": true // unclear if this is a boolean or a count of messages
}

// Document at /users/someUserId/contacts/anotherUserId
{
  "lastMsg": "Hi some user! How are you?",
  "lastMsgTime": /* Timestamp */,
  "unreadMsg": false
}

Using the above structure provides the following benefits:使用上述结构提供以下好处:

  • Significantly better network performance when hydrating the contacts list为联系人列表补充水分时,网络性能显着提高
  • Security rules can be used to ensure users can't read each others contacts lists安全规则可用于确保用户无法读取彼此的联系人列表
  • Security rules can be used to ensure a message stays private between the two users安全规则可用于确保消息在两个用户之间保持私密
  • Listening to another user's profile updates can be done without reading or being notified of any changes to their other private messages收听其他用户的个人资料更新可以在不阅读或收到其他私人消息的任何更改的情况下完成
  • You can partially fetch a user's message inbox rather than the whole list您可以部分获取用户的消息收件箱而不是整个列表
  • The contacts list is easy to update as two users updating the same contact entry is unlikely联系人列表很容易更新,因为两个用户不太可能更新同一个联系人条目
  • Easy to detect when a user's contact entry has been added, deleted or modified (such as receiving a new message or marking a message read)易于检测用户的联系人条目何时被添加、删除或修改(例如接收新消息或将消息标记为已读)

To fetch a user's contacts once in your functions.js library, you would use:要在您的functions.js库中获取用户的联系人一次,您可以使用:

// Utility function: Merges the data from an entry in a user's "contacts" collection with that user's data
const mergeContactEntrySnapshotWithUserSnapshot = (contactEntryDocSnapshot, contactDocSnapshot) => {
  const { lastMsg, lastMsgTime, unreadMsg } = contactEntryDocSnapshot.data();
  
  const baseContactData = {
    lastMessage: lastMsg,
    time: lastMsgTime,
    unreadMsg,
    userId: contactEntryDocSnapshot.id
  }
  
  if (!contactDocSnapshot.exists) {
    // TODO: Handle unknown/deleted users
    return {
      ...baseContactData,
      active: false,           // deleted users are inactive, nor do they
      email: null,             // have an email, image or display name
      img: null,
      userName: "Deleted user"
    };
  }
  
  const { active, emailId, imageUrl, userName } = contactDocSnapshot.data();
  
  return {
    ...baseContactData,
    active,
    email: emailId,
    img: imageUrl,
    userName
  };
}

// Utility function: Used to hydrate an entry in a user's "contacts" collection
const getContactFromContactsEntrySnapshot = (db, contactEntryDocSnapshot) => {
  return db.collection("users")
    .doc(contactEntry.userId)
    .get()
    .then((contactDocSnapshot) => mergeContactEntrySnapshotWithUserSnapshot(contactEntryDocSnapshot, contactDocSnapshot));
};

export const getUserContacts = (db, userId) => { // <-- note that db and userId are passed in
  return db.collection("users")
    .doc(userId)
    .collection("contacts")
    .get()
    .then((userContactsQuerySnapshot) => {
      return Promise.all( // <-- waits for each Promise to complete
        userContactsQuerySnapshot.docs // <-- used to get an array of entry snapshots that we can iterate over
          .map(getContactFromContactsEntrySnapshot.bind(null, db)); // for each contact, call the function (reusing db), returning a Promise with the data
      );
    });
}

Example Usage:示例用法:

getUserContacts(db, userId)
  .then((contacts) => console.log("Contacts data:", contacts))
  .catch((err) => console.error("Failed to get contacts:", err))
  
// OR

try {
  const contacts = await getUserContacts(db, userId);
  console.log("Contacts data:", contacts);
} catch (err) {
  console.error("Failed to get contacts:", err)
}

To fetch a user's contacts in a way where it's kept up to date, we first need to introduce a couple of utility useEffect wrappers (there are libraries for more robust implementations):为了以保持最新的方式获取用户的联系人,我们首先需要引入几个实用程序useEffect包装器(有一些库可以实现更强大的实现):

export const useFirestoreDocument = ({ db, path }) => {
  if (!db) throw new TypeError("Property 'db' is required");
  
  const [documentInfo, setDocumentInfo] = useState({ loading: true, snapshot: null, error: null });
  
  useEffect(() => {
    if (!path) {
      setDocumentInfo({ loading: false, snapshot: null, error: "Invalid path" });
      return;
    }
    
    // update loading status (as needed)
    if (!documentInfo.loading) {
      setDocumentInfo({ loading: true, snapshot: null, error: null });
    }
    
    return db.doc(path)
      .onSnapshot({
        next: (docSnapshot) => {
          setDocumentInfo({ loading: false, snapshot, error: null });
        },
        error: (err) => {
          setDocumentInfo({ loading: false, snapshot: null, error: err });
        }
      });
  }, [db, path]);
  
  return documentInfo;
}

export const useFirestoreCollection = ({ db, path }) => {
  if (!db) throw new TypeError("Property 'db' is required");
  
  const [collectionInfo, setCollectionInfo] = useState({ loading: true, docs: null, error: null });
  
  useEffect(() => {
    if (!path) {
      setCollectionInfo({ loading: false, docs: null, error: "Invalid path" });
      return;
    }
    
    // update loading status (as needed)
    if (!collectionInfo.loading) {
      setCollectionInfo({ loading: true, docs: null, error: null });
    }
    
    return db.collection(path)
      .onSnapshot({
        next: (querySnapshot) => {
          setCollectionInfo({ loading: false, docs: querySnapshot.docs, error: null });
        },
        error: (err) => {
          setCollectionInfo({ loading: false, docs: null, error: err });
        }
      });
  }, [db, path]);
  
  return collectionInfo;
}

To use that method to hydrate a contact, you would call it from a ContactEntry component:要使用该方法来水合联系人,您可以从ContactEntry组件中调用它:

// mergeContactEntrySnapshotWithUserSnapshot is the same as above

const ContactEntry = ({ db, userId, key: contactId }) => {
  if (!db) throw new TypeError("Property 'db' is required");
  if (!userId) throw new TypeError("Property 'userId' is required");
  if (!contactId) throw new TypeError("Property 'key' (the contact's user ID) is required");

  const contactEntryInfo = useFirestoreDocument(db, `/users/${userId}/contacts/${contactId}`);
  const contactUserInfo = useFirestoreDocument(db, `/users/${contactId}`);
  
  if ((contactEntryInfo.loading && !contactEntryInfo.error) && (contactUserInfo.loading && !contactUserInfo.error)) {
    return (<div>Loading...</div>);
  }
  
  const error = contactEntryInfo.error || contactUserInfo.error;
  if (error) {
    return (<div>Contact unavailable: {error.message}</div>);
  }
  
  const contact = mergeContactEntrySnapshotWithUserSnapshot(contactEntryInfo.snapshot, contactUserInfo.snapshot);

  return (<!-- contact content here -->);
}

Those ContactEntry components would be populated from a Contacts component:这些ContactEntry组件将从Contacts组件中填充:

const Contacts = ({db}) => {
  if (!db) throw new TypeError("Property 'db' is required");
  
  const { user } = useFirebaseAuth();
  const contactsCollectionInfo = useFirestoreCollection(db, user ? `/users/${user.uid}/contacts` : null);
  
  if (!user) {
    return (<div>Not signed in!</div>);
  }
  
  if (contactsCollectionInfo.loading) {
    return (<div>Loading contacts...</div>);
  }
  
  if (contactsCollectionInfo.error) {
    return (<div>Contacts list unavailable: {contactsCollectionInfo.error.message}</div>);
  }
  
  const contactEntrySnapshots = contactsCollectionInfo.docs;
  
  return (
    <>{
      contactEntrySnapshots.map(snapshot => {
        return (<ContactEntry {...{ db, key: snapshot.id, userId: user.uid }} />);
      })
    }</>
  );
}

Example Usage:示例用法:

const db = firebase.firestore();
return (<Contacts db={db} />);

Your code seems to be not written with async/await or promise like style您的代码似乎不是用 async/await 或 promise 之类的风格编写的

eg contactDetailsArr will be returned as empty array例如 contactDetailsArr 将作为空数组返回

also onSnapshot creates long term subscription to Firestore collection and could be replaced with simple get() onSnapshot 也会创建对 Firestore 集合的长期订阅,并且可以用简单的 get() 替换

在此处输入图像描述

See example on firestore https://firebase.google.com/docs/firestore/query-data/get-data#web-version-9_1请参阅Firestore https://firebase.google.com/docs/firestore/query-data/get-data#web-version-9_1上的示例

在此处输入图像描述

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 如何通过 redux-observable/rxjs 使用 Firestore 实时更新(onSnapshot)? - How to use Firestore realtime updates (onSnapshot) with redux-observable/rxjs? 没有此类数据时如何处理返回的 Firestore 查询? - How to handle a returned Firestore query when there is not such data? 如何使用 Firestore 按值排序 - How to orderByValue onSnapshot using Firestore 如何使用 AngularFire 观察 Firestore 文档? `onSnapshot() 不工作? - How to observe a Firestore document with AngularFire? `onSnapshot() not working? Firestore onSnapshot 反应 - Firestore onSnapshot in react 当存在应返回的数据时,Firestore 查询返回空数组 - Firestore query returns empty array when there is data that should be returned 如何从 Firestore 中的嵌套 map 中提取数据? - How to extract data from a nested map in firestore? 使用来自 Firestore 实时侦听器的数据更新图表 - Updating chart with data from Firestore realtime listener 分页如何与实时 Firestore 查询一起工作? - How paging work with realtime firestore query? 在具有依赖项的 useEffect 中设置 Firestore onSnapshot 侦听器会导致多个订阅吗? - Will setting up a Firestore onSnapshot listener in a useEffect with dependencies result in multiple subscriptions?
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM