React Hooks + Firebase Firestore onSnapshot - 正确使用带有反应钩子的 Firestore 监听器

Posted

技术标签:

【中文标题】React Hooks + Firebase Firestore onSnapshot - 正确使用带有反应钩子的 Firestore 监听器【英文标题】:React Hooks + Firebase Firestore onSnapshot - Correct use of a firestore listener with react hooks 【发布时间】:2021-03-30 18:47:45 【问题描述】:

问题

假设您有一个带有数据库侦听器的屏幕,该侦听器是在 useEffect 中创建的,该侦听器的目的是增加屏幕中的一个计数器,就像这样:

(仅使用 useEffect 没有依赖的钩子)

function MyScreen(props) 
     const [magazinesCounter, setMagazinesCounter] = useState(0);

     const handleMagazinesChanges = (snapshot) => 
         const changes = snapshot.docChanges();

         let _magazinesCounter = magazinesCounter;
       
         changes.forEach((change) => 
            if (change.type === "added") 
               _magazinesCounter  += 1;
            
            if (change.type === "removed") 
               _magazinesCounter  -= 1;
            
         );
         
         setMagazinesCounter(_magazinesCounter);  
     ;

     useEffect(() => 
         const query = props.db.collection("content")
                        .where("type", "==", "magazine");
         
         // Create the DB listener
         const unsuscribe = query.onSnapshot(handleMagazinesChanges, (err) => );
         return () => 
            unsuscribe();
         
     , []);

正如您在此处看到的,这将不起作用,因为在状态更新时不会重新创建 useEffect 的侦听器中使用的 handleMagazinesChanges...

因此,我尝试解决将magazinesCounter 作为依赖项传递给useEffect 的问题,如下所示:

(使用 useEffect hook 将修改后的状态作为依赖)

 useEffect(() => 
     // ... the same stuff
 , [magazinesCounter]);

但是这样一来,我们将进入一个无限循环,因为监听器将被重新创建,并且

 if (change.type === "added") 
     _magazinesCounter  += 1;
 
 ...
 setMagazinesCounter(_magazinesCounter);

将再次执行,一次又一次......

Pd:另外,将 handleMagazinesChanges 函数包装在 useCallback 中,并以 MagazinesCounter 作为依赖项,然后将该函数作为依赖项传递给 useEffect,将具有相同的效果...

那么,我该如何解决这种情况?我知道,如果我们使用 useRef 对同一数据进行辅助引用,我们可以成功执行此操作,避免死循环。但是,有没有其他更好的方法来做到这一点?我的意思是,没有状态 + 对最新数据的引用:

(无依赖的useEffect + useRef)

// This works good, but is there any other better solution to this problem?
function MyScreen(props) 
     const [magazinesCounter, setMagazinesCounter] = useState(0);

     const magazinesCounterRef = useRef(magazinesCounter);

     const handleMagazinesChanges = (snapshot) => 
         const changes = snapshot.docChanges();

         changes.forEach((change) => 
            if (change.type === "added") 
               magazinesCounterRef.current  += 1;
            
            if (change.type === "removed") 
               magazinesCounterRef.current  -= 1;
            
         );

         setMagazinesCounter(magazinesCounterRef.current);  
     ;

     useEffect(() => 
         const query = props.db.collection("content")
                        .where("type", "==", "magazine");

         // Create the DB listener
         const unsuscribe = query.onSnapshot(handleMagazinesChanges, (err) => );
         return () => 
            unsuscribe();
         
     , []);

有什么想法吗?谢谢。

Pd:也许这是要走的路,但我想有更好的方法,而无需创建辅助变量。

【问题讨论】:

【参考方案1】:

为了实现这一目标,我有两条建议:

    将回调移动到 useEffect 内以避免每次渲染时重新创建函数 在设置状态时使用回调来获取当前值(参见React Docs here)以避免在状态改变时需要重新创建回调

将这些与您当前的代码一起使用:

function MyScreen(props) 
     const [magazinesCounter, setMagazinesCounter] = useState(0);

     useEffect(() => 
         // Moved inside "useEffect" to avoid re-creating on render
         const handleMagazinesChanges = (snapshot) => 
             const changes = snapshot.docChanges();

             // Accumulate differences
             let difference  = 0;
             changes.forEach((change) => 
                if (change.type === "added") 
                   difference  += 1;
                
                if (change.type === "removed") 
                   difference  -= 1;
                
             );
             
             // Use the setState callback 
             setMagazinesCounter((currentMagazinesCounter) => currentMagazinesCounter + difference);  
         ;

         const query = props.db.collection("content")
                        .where("type", "==", "magazine");
         
         // Create the DB listener
         const unsuscribe = query.onSnapshot(handleMagazinesChanges, 
  err => console.log(err));
         return () => 
            unsuscribe();
         
     , []);

【讨论】:

以上是关于React Hooks + Firebase Firestore onSnapshot - 正确使用带有反应钩子的 Firestore 监听器的主要内容,如果未能解决你的问题,请参考以下文章

React Hooks 不适用于 Firebase Cloud Functions 错误:不变违规:无效的钩子调用

React + Firebase 使用钩子?

React Hooks + Firebase Firestore onSnapshot - 正确使用带有反应钩子的 Firestore 监听器

React Hooks:如何在 useEffect 中设置状态?

错误:使用 react-hooks 时超出最大更新深度

React-firestore-hooks 从云 Firestore 中获取数据库记录