React Hooks + Firebase Firestore onSnapshot - 通过 React Hooks 正确使用 Firestore 侦听器

Vic*_*ina 3 javascript reactjs react-native google-cloud-firestore react-hooks

问题

想象一下,您有一个带有数据库侦听器的屏幕,该侦听器是在 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();
         }
     }, []);
}
Run Code Online (Sandbox Code Playgroud)

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

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

(使用 useEffect 钩子并将修改后的状态作为依赖项)

 useEffect(() => {
     // ... the same stuff
 }, [magazinesCounter]);
Run Code Online (Sandbox Code Playgroud)

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

 if (change.type === "added") {
     _magazinesCounter  += 1;
 }
 ...
 setMagazinesCounter(_magazinesCounter);
Run Code Online (Sandbox Code Playgroud)

将再次被处决,再次……

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();
         }
     }, []);
}
Run Code Online (Sandbox Code Playgroud)

有任何想法吗?谢谢。

Pd:也许这是可行的方法,但我认为有更好的方法,无需创建辅助变量。

sma*_*oes 5

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

  1. 将回调移至内部useEffect以避免每次渲染时重新创建函数
  2. 设置状态时使用回调来获取当前值(请参阅此处的 React 文档),以避免在状态更改时需要重新创建回调

将它们与您当前的代码一起使用:

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();
         }
     }, []);
}
Run Code Online (Sandbox Code Playgroud)