Search code examples
reactjsfirebasegoogle-cloud-firestorereact-hooksuse-effect

useEffect rendering multiple times even when data is not changing


This useEffect is rendering one time if dependency array is empty but multiple times if i put folderRef in dependency array. I want to render the component only when I add or delete some folder. Please Help

  import React, { useState, useEffect , useRef } from "react";
import { db } from "../firebase";
import { collection, getDocs } from "firebase/firestore";
import FolderData from "./FolderData";

function ShowFolder(props) {
  const [folders, setFolders] = useState([]);
  const folderRef = useRef(collection(db, "folders"));

  useEffect(() => {
    const getData = async () => {
      const data = await getDocs(folderRef.current);
      const folderData = data.docs.map((doc) => {
        return { id: doc.id, data: doc.data() };
      });
      console.log(folderData);
      setFolders(folderData);
    };
    getData();
  }, [folderRef]);

  return (
    <div className="container md:px-4 mx-auto py-10">
      <div className="md:grid lg:grid-cols-6 md:grid-cols-3 mlg:grid-cols-3 md:gap-10 space-y-6 md:space-y-0 px-1 md:px-0 mx-auto">
        {folders.map((folder) => {
          return (
            <div key={folder.id}>
              {folder.data.userId === props.userId && (
                <div>
                  <FolderData key={folder.id} folder={folder} />
                </div>
              )}
            </div>
          );
        })}
      </div>
    </div>
  );
}

export default ShowFolder;

Solution

  • You redeclare folderRef each render cycle, so if you include it in the useEffect hook's dependency array it will trigger render looping.

    If you don't refer to folderRef anywhere else in the component then move it into the useEffect hook callback to remove it as an external dependnecy.

    const [folders, setFolders] = useState([]);
    
    useEffect(() => {
      const folderRef = collection(db, "folders");
    
      const getData = async () => {
        const data = await getDocs(folderRef);
        const folderData = data.docs.map((doc) => {
          return { id: doc.id, data: doc.data() };
        });
        console.log(folderData);
        setFolders(folderData);
      };
    
      getData();
    }, []);
    

    Or store it in a React ref so it can be safely referred to as a stable reference.

    const [folders, setFolders] = useState([]);
    const folderRef = useRef(collection(db, "folders"));
    
    useEffect(() => {
      const getData = async () => {
        const data = await getDocs(folderRef.current);
        const folderData = data.docs.map((doc) => {
          return { id: doc.id, data: doc.data() };
        });
        console.log(folderData);
        setFolders(folderData);
      };
    
      getData();
    }, [folderRef]);
    

    Update

    I've gathered that you are updating the folders collection elsewhere in your app and want this component to "listen" for these changes. For this you can implement an onSnapshot listener.

    It may look similar to the following:

    const [folders, setFolders] = useState([]);
    
    useEffect(() => {
      const unsubscribe = onSnapshot(
        collection(db, "folders"),
        (snapshot) => {
          const folderData = [];
          snapshot.forEach((doc) => {
            folderData.push({
              id: doc.id,
              data: doc.data(),
            });
          });
          setFolders(folderData);
        },
      );
    
      // Return cleanup function to stop listening to changes
      // on component unmount
      return unsubscribe;
    }, []);