#StackBounty: #javascript #reactjs #firebase #google-cloud-firestore #react-hooks Reading firestore sub-collection data in react

Bounty: 50

I’m trying to figure out how to read firestore sub collection data from react.

I have seen this blog that describes how to do it and am now trying to figure out how to implement the logic of it.

I have a parent collection called glossary and a sub collection within it called relatedTerms.

I have a form that works to correctly submit data to the glossary (parent) and relatedTerms (sub) collections.

I’m struggling to find a way to use react hooks to generate the list of glossary terms (this collection holds data) with associated related terms sub collection data).

My current attempt at doing that is:

function useGlossaryTerms() {
    const [glossaryTerms, setGlossaryTerms] = useState([])
    
    
   // First useEffect - to set the glossaryTerm (the parent document) 
    useEffect(async() => {
      await firebase
        .firestore()
        .collection("glossary")
        .orderBy('term')
        .onSnapshot(snapshot => {
          const glossaryTerms = snapshot.docs.map(doc => ({
            id: doc.id,
            ...doc.data(),
          }))
          setGlossaryTerms(glossaryTerms)
        });
        // setRelatedGlossaryTerms(glossaryTerms)
    }, [])
    return glossaryTerms;
  };

function useRelatedGlossaryTerms() {
const [relatedGlossaryTerms, setRelatedGlossaryTerms] = useState([])
// Second useEffect - to set the relatedGlossaryTerm (the sub collection document)
  useEffect(async() => {
    await firebase
      .firestore()
      .collection("glossary")
      .doc()
      // {console.log("testing:", glossaryTerms)}
      .collection('relatedTerms')
      
      //.where("glossaryId" === "SQ3Qf0A65ixNbF07G2")
      .onSnapshot(snapshot => {
        const relatedGlossaryTerms = snapshot.docs.map(doc => ({
          id: doc.id,
          ...doc.data(),
        }))
        setRelatedGlossaryTerms(relatedGlossaryTerms)
      });
     
      // setRelatedGlossaryTerms(glossaryTerms)
  }, [glossaryTerms])
  return relatedGlossaryTerms;
};


  

const GlossaryTerms = () => {

    const glossaryTerms = useGlossaryTerms()
    const relatedGlossaryTerms = useRelatedGlossaryTerms()
        
    const classes = useStyles();

    return ( 
        
            <div className={classes.root}>
            
            {glossaryTerms.map(glossaryTerm => {

{console.log(‘testing log of glossaryTerms’, glossaryTerms)}
return (

                    <Typography className={classes.heading}>{glossaryTerm.term}</Typography>
                    
                    <div>
                    
                    {glossaryTerm.category.map(category => (
                        <Typography className={classes.secondaryHeading}>
                        {category.label}
                        </Typography>
                    )

                    )}
                    
                   
                    <div>    
                        <Typography variant="subtitle2" className={classes.heading2}>Meaning</Typography>
                        <Typography>{glossaryTerm.definition}</Typography>
                    </div>
                       
                    <div>
                    {relatedGlossaryTerms ? (
                      <ul>
                        {relatedGlossaryTerms.map(relatedGlossaryTerm => (
                          <div key={relatedGlossaryTerm.id}>
                            <Typography variant="caption">
                            Placeholder title {relatedGlossaryTerm.title} | 
                            Placeholder description {relatedGlossaryTerm.description} 
                            </Typography>
                          </div>  
                        ))}
                      </ul>
                    ) : null}


                                                {
                            glossaryTerm.templates ? (
                              <div>    
                                <p><Typography variant="caption" >Related Templates</Typography></p>
                                {glossaryTerm.templates.map(template => (
                                    
                                    <Link to="" className="bodylinks"  key={template.id}>
                                      <p><Typography variant="caption" >{template.title}
                                      </Typography></p>
                                    </Link>
                                ))}
                              </div>
                            ) : null
                          }
                    </div>
                    <div>
                        
                 

            )
        })}
            </div>
            
        </div>   
</div>
     );
}
 
export default GlossaryTerms;

When I try this, I don’t get any errors in the console, but, the rendered output is incorrect and confusing.

The glossary currently has 2 documents in that collection. One of the documents has a relatedTerms subcollection which also has 2 documents. The other has no sub collection entries (other than a glossaryId field which I add to the setSubmitting form add so that I could try matching the string ids to filter sub collection documents – this attempt is outlined in the history of this post).

Whilst the glossary data is correctly rendered, the sub collection data for relatedTerms is not rendered at all – BUT the placeholder text is repeated for each instance of the parent collection (2 documents). I currently have 2. If I add another entry to the sub collection to test this, the 2 placeholder sets of text are repeated in each glossary document rendered.

The image below shows the rendered output.
enter image description here

The image below shows the content of the glossary collection.

enter image description here

The image below shows the content of the relatedTerms sub collection.

enter image description here

This is confusing, because the placeholder text is repeated the same number of times as is equal to the number of parent collection document entries. It’s not testing for sub collection values.

It’s also incorrect because I can’t find a way to join the relatedTerm to its relevant glossaryTerm collection entry. Each item in the list should render its sub collection content only.

When I try logging the relatedGlossaryTerms, as follows:

 {console.log("testing log of relatedGlossaryTerms", relatedGlossaryTerms)}
                    {relatedGlossaryTerms ? (
                      
                      <ul>
                        {relatedGlossaryTerms.map(relatedGlossaryTerm => (
                    

I get:

enter image description here

This log is confusing to me because it’s supposed to be the content of the sub collection. Instead it logs the parent collection.

I can’t find a way to log the value of the relatedGlossaryTerms.

When I try adding console log statements to the snapshot, I get errors that I can’t understand. Plenty of other posts show this as a correct method to use to log the values. I tried adding curly braces, but get the same issue.

useEffect(() => {
    firebase
      .firestore()
      .collection("glossary")
      .doc()
      // {console.log("testing:", glossaryTerms)}
      .collection('relatedTerms')
      // .where("glossaryId" === "SQ3Qf0A65ixNbF07G2")
      .onSnapshot(snapshot => {
        const relatedGlossaryTerms = snapshot.docs.map(doc => ({
          
      
          id: doc.id,
          ...doc.data(),
          // console.log('test useEffect setting for relatedGlossaryTerms', doc),
          // console.log(relatedGlossaryTerms)
        }))
        setRelatedGlossaryTerms(relatedGlossaryTerms)
        
      });
  

When I try logging the values of glossaryTerms and relatedGlossaryTerms in the return statement, I get:

enter image description here
enter image description here
Trying Yoruba’s suggestion

I tried Yoruba’s suggestion to use get instead of snapshot. Although it does not make sense to me as to why I can’t use snapshot without mobx, I tried it.

    useEffect(() => {
      firebase
        .firestore()
        .collection("glossary")
        .orderBy('term')
        .get()
        .then(snapshot => {
          const glossaryTerms = snapshot.docs.map(doc => ({
            id: doc.id,
            ...doc.data(),
          }))

          setGlossaryTerms(glossaryTerms)
        });
        // setRelatedGlossaryTerms(glossaryTerms)
    }, [])
    return glossaryTerms;
  

  useEffect(() => {
    firebase
      .firestore()
      .collection("glossary")
      .doc()
      // {console.log("testing:", glossaryTerms)}
      .collection('relatedTerms')
      // .where("glossaryId" === "SQ3Qf0A65ixNbF07G2")
      .get()
      .then(snapshot => {
        const relatedGlossaryTerms = snapshot.docs.map(doc => ({
          
      
          id: doc.id,
          ...doc.data(),
          
        }))
        setRelatedGlossaryTerms(relatedGlossaryTerms)
        
      });
     
      // setRelatedGlossaryTerms(glossaryTerms)
  }, [glossaryTerms])
  return relatedGlossaryTerms;
};

It doesn’t make any difference. No new console errors. No difference to the rendering problems outlined above.

ALL PREVIOUS ATTEMPTS

 onSubmit={(values, { setSubmitting }) => {
                     setSubmitting(true);
                     
                    //  firestore.collection("glossary").doc().set({
                    //   ...values,
                    //   createdAt: firebase.firestore.FieldValue.serverTimestamp()
                    //   })
                    // .then(() => {
                    //   setSubmitionCompleted(true);
                    // });
                  // }}
                  const newDocRef = firestore.collection("glossary").doc() // auto generated doc id saved here
  let writeBatch = firestore.batch();
  {console.log("logging values:", values)};
  writeBatch.set(newDocRef,{
    term: values.term,
    definition: values.definition,
    category: values.category,
    context: values.context,
    createdAt: firebase.firestore.FieldValue.serverTimestamp()
  });
  writeBatch.set(newDocRef.collection('relatedTerms').doc(),{
    // dataType: values.dataType,
    // title: values.title,
    glossaryId: newDocRef.id,
    ...values.relatedTerms
    // description: values.description,
  })
  writeBatch.commit()
    .then(() => {
      setSubmitionCompleted(true);
    });
}}
  

No error message is shared when I try this, the glossaryId just gets ignored. It remains a mystery to me what the batch concept is- I thought it only ran if all the instructions could be performed.

I’m trying to figure out how to access the data stored in the sub collection. My current attempt at doing that is based on the example in the post linked above.

import React, { useState, useEffect } from 'react';
import {Link } from 'react-router-dom';
import Typography from '@material-ui/core/Typography';
import firebase, { firestore } from "../../../../firebase.js";
import { makeStyles } from '@material-ui/core/styles';
import clsx from 'clsx';

const useStyles = makeStyles((theme) => ({
  root: {
    width: '100%',
    marginTop: '8vh',
    marginBottom: '5vh'
  },
 
}));


function useGlossaryTerms() {
    const [glossaryTerms, setGlossaryTerms] = useState([])
    const [relatedGlossaryTerms, setRelatedGlossaryTerms] = useState([])
    
    
    useEffect(() => {
      firebase
        .firestore()
        .collection("glossary")
        .orderBy('term')
        .onSnapshot(snapshot => {
          const glossaryTerms = snapshot.docs.map(doc => ({
            id: doc.id,
            ...doc.data(),
          }))
          setGlossaryTerms(glossaryTerms)
        });
        // setRelatedGlossaryTerms(glossaryTerms)
    }, [])
    return glossaryTerms;
  }

  const relatedTermsList = (glossaryTerm) => {
    setGlossaryTerms(glossaryTerm);
    firebase.firestore().collection('glossary').doc(glossaryTerm.id).collection('relatedTerms').get()
      .then(response => {
        const relatedGlossaryTerms = [];
        response.forEach(document => {
          const relatedGlossaryTerm = {
            id: document.id,
            ...document.data()
          };
          relatedGlossaryTerms.push(relatedGlossaryTerm);
        });
        setRelatedGlossaryTerms(relatedGlossaryTerms);
      })
      .catch(error => {
        // setError(error);
      });
  }
  

const GlossaryTerms = () => {

    const glossaryTerms = useGlossaryTerms()
    const relatedGlossaryTerms = useGlossaryTerms()
        
    const classes = useStyles();

    return ( 
        <div>
            {glossaryTerms.map(glossaryTerm => {
                return (
                    
               {glossaryTerm.term}
                    {glossaryTerm.category.map(category => (
                        
                        {category.label}
                        
                    )

                    )}
                    
                    </div>
               {glossaryTerm.definition}

                    {glossaryTerm ? (
                      <ul>
                        {relatedGlossaryTerms.map(relatedGlossaryTerm => (
                          <li key={relatedGlossaryTerm.id}>
                            {relatedGlossaryTerm.title} | {relatedGlossaryTerm.description} 
                          </li>
                        ))}
                      </ul>
                    ) : null}

                    
            )
        })}
            </div>
            
         

     );
}
 
export default GlossaryTerms;

When I try this, I get error messages saying that in my relatedTermsList const, the definitions of setGlossaryTerms and setRelatedGlossaryTerms are undefined.

Each of these errors are odd to me because setRelatedGlossaryTerms is defined in the same way as setGlossaryTerms. I don’t understand why it is unrecognisable and setGlossaryTerms is used in the useEffect without any issue.

NEXT ATTEMPT

I tried using a second useEffect function that takes a glossaryTerm.

function useGlossaryTerms() {
    const [glossaryTerms, setGlossaryTerms] = useState([])
    const [relatedGlossaryTerms, setRelatedGlossaryTerms] = useState([])
    
    
    useEffect(() => {
      firebase
        .firestore()
        .collection("glossary")
        .orderBy('term')
        .onSnapshot(snapshot => {
          const glossaryTerms = snapshot.docs.map(doc => ({
            id: doc.id,
            ...doc.data(),
          }))
          setGlossaryTerms(glossaryTerms)
        });
        // setRelatedGlossaryTerms(glossaryTerms)
    }, [])
    return glossaryTerms;
  }

  useEffect((glossaryTerm) => {
    firebase
      .firestore()
      .collection("glossary")
      .doc(glossaryTerm.id)
      .collection('relatedTerms')
      .onSnapshot(snapshot => {
        const relatedGlossaryTerms = snapshot.docs.map(doc => ({
          id: doc.id,
          ...doc.data(),
        }))
        setRelatedGlossaryTerms(relatedGlossaryTerms)
      })
      .catch(error => {
        // setError(error);
      });
  });
  

const GlossaryTerms = () => {

    const glossaryTerms = useGlossaryTerms()
    const relatedGlossaryTerms = useGlossaryTerms()
        
    const classes = useStyles();

    return ( 

I don’t understand why, but the error is that setRelatedGlossaryTerms is not defined – where it’s used in the second useEffect.

NEXT ATTEMPT

This attempt does not produce any errors in the console but the result is incorrect.

I have two useEffect functions as follows:

function useGlossaryTerms() {
    const [glossaryTerms, setGlossaryTerms] = useState([])
    const [relatedGlossaryTerms, setRelatedGlossaryTerms] = useState([])
    
    
    useEffect(() => {
      firebase
        .firestore()
        .collection("glossary")
        .orderBy('term')
        .onSnapshot(snapshot => {
          const glossaryTerms = snapshot.docs.map(doc => ({
            id: doc.id,
            ...doc.data(),
          }))
          setGlossaryTerms(glossaryTerms)
        });
        // setRelatedGlossaryTerms(glossaryTerms)
    }, [])
    return glossaryTerms;
  

  useEffect((glossaryTerms) => {
    firebase
      .firestore()
      .collection("glossary")
      .doc("glossaryTerms.id") //I also tried .doc(glossaryTerms.id)
      .collection('relatedTerms')
      .onSnapshot(snapshot => {
        const relatedGlossaryTerms = snapshot.docs.map(doc => ({
          id: doc.id,
          ...doc.data(),
        }))
        setRelatedGlossaryTerms(relatedGlossaryTerms)
      });
      // setRelatedGlossaryTerms(glossaryTerms)
  }, [])
  return relatedGlossaryTerms;
};


  

const GlossaryTerms = () => {

    const glossaryTerms = useGlossaryTerms()
    const relatedGlossaryTerms = useGlossaryTerms()
        
    const classes = useStyles();

    return ( 

The intention behind these two useEffects is that the first one finds the glossaryTerm and then the second one uses the glossaryTerm.id to find if there are any related terms and then sets the state of the relatedGlossaryTerms array with any related terms.

Then in the render, I have:

 {glossaryTerm.category.map(category => (
                        <Typography className={classes.secondaryHeading}>
                        {category.label}
                        </Typography>
                    )

The above works to correctly find the glossaryTerm.

{relatedGlossaryTerms ? (
                      <ul>
                        {relatedGlossaryTerms.map(relatedGlossaryTerm => (
                          <div>
                            <Typography variant="caption">
                            Placeholder title {relatedGlossaryTerm.title} | 
                            Placeholder description {relatedGlossaryTerm.description} 
                            </Typography>
                          </div>  
                        ))}
                      </ul>
                    ) : null}

The intention is for the above to look to see if there are any relatedGlossaryTerms (which should be the result of finding the sub collection inside the glossaryTerm.

This doesn’t produce any console errors, but it does 2 things that need to be corrected.

  1. it renders the same result for each glossaryTerm (regardless of whether it actually has any relatedGlossaryTerms.
  2. It does not render the relatedGlossaryTerm data. The picture below shows the placeholder text printed, repeatedly (as many times as is equal to the total number of related terms in all glossaryTerms documents combined).

enter image description here

I’m trying to find a way to use the glossaryTerm document id to find the sub collection data and render it only inside the relevant glossaryTerm item.

Next Attempt

I also tried:

useEffect((glossaryTermsId) => {
    firebase
      .firestore()
      .collection("glossary")
      .doc(glossaryTermsId)
      .collection('relatedTerms')
      .where(glossaryTermsId === "glossaryId") // I also tried .where(glossaryTerms.id === "glossaryId)
      .onSnapshot(snapshot => {
        const relatedGlossaryTerms = snapshot.docs.map(doc => ({
          id: doc.id,
          ...doc.data(),
        }))

    setRelatedGlossaryTerms(relatedGlossaryTerms)
  });

I have added a field to the sub collection called glossaryId to try to use the where query to filter the sub collection (although even if this worked, which it doesn’t) – I don’t want to search every glossaryTerm document to get the right relatedTerms.

enter image description here
When I try

useEffect((glossaryTerms) => {
    firebase
      .firestore()
      .collection("glossary")
      .doc(glossaryTerms)
      .collection('relatedTerms')
      .where("glossaryId" === "JsSQ3Qf0A65ixNbF07G2")
      .onSnapshot(snapshot => {
        const relatedGlossaryTerms = snapshot.docs.map(doc => ({
          id: doc.id,
          ...doc.data(),
        }))
        setRelatedGlossaryTerms(relatedGlossaryTerms)
      });
  

I still get the same result as shown in the picture above (just the placeholder text prints – the same way in all of the glossaryTerms – it just ignores the query parameter of the glossaryId attribute in the sub collection document.

NEXT ATTEMPT

I’m scratching for ideas for things to try now. I’ve tried adding glossaryTerms as a dependency to the second useEffect and I tried giving the glossaryTermsId as a prop (instead of the glossaryTerms object), as follows:

useEffect((glossaryTermsId) => {
    firebase
      .firestore()
      .collection("glossary")
      .doc(glossaryTermsId)
      .collection('relatedTerms')
      // .where("glossaryId" === "JsSQ3Qf0A65ixNbF07G2")
      .onSnapshot(snapshot => {
        const relatedGlossaryTerms = snapshot.docs.map(doc => ({
          id: doc.id,
          ...doc.data(),
        }))
        setRelatedGlossaryTerms(relatedGlossaryTerms)
      });
      // setRelatedGlossaryTerms(glossaryTerms)
  }, [glossaryTerms])
  return relatedGlossaryTerms;
};

Neither of these things make any difference to the outcomes shown above.

When I try to log the content of glossaryTerm in the render – one strange thing is that the sub collection data is not displayed. I only have 2 test entries in the glossary collection. 1 has a related term and the other doesn’t. Neither is visible in the console log. I think this is because of shallow queries. But, I can’t then find a way to link the collection query to the sub collection data.

enter image description here

Next attempt

Taking Bennett’s advice, I removed the parameters from the useEffect as follows:

useEffect(() => {
    firebase
      .firestore()
      .collection("glossary")
      .doc()
      // {console.log("testing:", glossaryTerms)}
      .collection('relatedTerms')
      
      // .where("glossaryId" === "JSQ3Qf0A65ixNbF07G2")
      .onSnapshot(snapshot => {
        const relatedGlossaryTerms = snapshot.docs.map(doc => ({
          id: doc.id,
          ...doc.data(),
        }))
        setRelatedGlossaryTerms(relatedGlossaryTerms)
      });
     
      // setRelatedGlossaryTerms(glossaryTerms)
  }, [glossaryTerms])
  return relatedGlossaryTerms;
};

When I try this, it still renders the same output as shown in the image above.

When I uncomment the where query (which is the doc id of a document which has a sub collection for relatedTerms, I’m expecting to be able to get to the subcolleciton data (even if its in all the glossaryTerms, instead of just the one to which the sub collection belongs).

Instead, I get the placeholder text, rendered the number of times as is equal to the total number of related terms recorded in all sub collections in all the glossaryTerms documents (but no data).

Then when I change the string, so it should evaluate to null, I get the same rendered output (still prints the placeholders).


Get this bounty!!!

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.