0

I'm using React with Firebase to fetch data from firestore and display it to the user. Within a useEffect hook, I fetch the data and add it to an array of json objects. However, when adding a new element to the array, the previous one gets deleted. Below will be relevant portions of code.

const [items, setItems] = useState([]);
const [isLoading, setLoading] = useState(true);
const { currentUser } = useAuth();

function addItemToList(i) {
  const updatedList = [
    ...items,
    {
      data: i.data(),
      ref: i.ref
    }
  ]

  return updatedList;
} 
useEffect(() => {
  firestore.collection('items').where('uid', '==', currentUser.uid).get().then((itemSnapshot) => {
    itemSnapshot.forEach((doc) => {
      setItems(addItemToList(doc));
    })
  })
  setLoading(false);
}, []);

I've also tried updating the items list within the useEffect hook, but it produces the same bug.

bob marsh
  • 15
  • 2

5 Answers5

0

You need to add "addItemToList" to the dependencies of the useEffect hook.

Because you haven't done this, the effect references an old version of that function which references an old version of the items array.

Calin Leafshade
  • 1,195
  • 1
  • 12
  • 22
  • Thanks for your comment, but when I tried this solution, the useEffect hook would keep getting called, resulting in an infinite loop causing the app to crash. – bob marsh Aug 02 '21 at 04:19
  • Thats because the addItemsToList function would be a different one everytime. To fix this you need to wrap is in `useCallback` to make a consistent callback that doesnt change every render. You can avoid all this however just by using the callback version of the setState function. `setItems(old => old.concat(newItems))` – Calin Leafshade Aug 02 '21 at 21:50
0

You just use itemSnapshot.map like this:

firestore.collection('items').where('uid', '==', currentUser.uid).get().then((itemSnapshot) => {
    setItems(
      itemSnapshot.map((doc) => ({
        data: i.data(),
        ref: i.ref,
      })),
    );
  })
Viet
  • 12,133
  • 2
  • 15
  • 21
  • Thanks for your comment. The solution you suggested would not work since `itemSnapshot` is not an array. I tried rearranging my code to look more like your's (fixing issues ad needed), but I still run into the same issue. – bob marsh Aug 02 '21 at 04:20
  • `itemSnapshot` is not an array so why you can use `itemSnapshot.forEach`? – Viet Aug 02 '21 at 04:22
  • `itemSnapshot` is a method provided by firestore that allows you to iterate over all the documents fetched from the database. The data type, according to my editor, is `firebase.firestore.QuerySnapshot` – bob marsh Aug 02 '21 at 04:26
  • If `itemSnapshot` is not an array. You cannot use `forEach` – Viet Aug 02 '21 at 04:33
  • `forEach` is the specified way of getting all the documents fetched from the database, as per google's own documentation. – bob marsh Aug 02 '21 at 04:36
  • convert it to array – Viet Aug 02 '21 at 04:42
0

Let me take a moment to explain what is happening here.

Your have to think in your react component as a "Snapshot", where a snapshot is the result of a render. Each snapshot points to a state created by useState and only to that one. What does this mean?

The first time your component renders (first snapshot) the items variable returned by useState is pointing to an empty array and as long as that snapshot is alive it would be pointing to that array.

So, lets take a look to the function called by useEffect

  firestore.collection('items').where('uid', '==', currentUser.uid).get().then((itemSnapshot) => {
    itemSnapshot.forEach((doc) => {
      setItems(addItemToList(doc));
    })
  })
  setLoading(false);

you are calling the set function for items once per elemtent in firebase query result and each call is seting a value that is the result of calling addItemToList

function addItemToList(i) {
  const updatedList = [
    ...items,
    {
      data: i.data(),
      ref: i.ref
    }
  ]

  return updatedList;
} 

the point here is that you are always destructuring ...items which is always the items variable that the snapshot of the component is pointing to and it would not be updated until the component re rederds.

So, basically you all aways doing updatedList = [ ...[], { ... } ] becauses items has to wait to take update his value.

@Viet already responded with something good, which I think it's the best option

mapping the result of the query to an array and updating the array with the result of the map a parameter.

useEffect(() => {
  firestore.collection('items').where('uid', '==', currentUser.uid).get().then((itemSnapshot) => {
    const result = [];
      itemSnapshot.forEach((doc) => {
        result.push({
          data: i.data(),
          ref: i.ref
        });
      })
    setItems(result);
  })
  setLoading(false);
}, []);
Herbie Vine
  • 1,643
  • 3
  • 17
  • 32
sasal
  • 201
  • 1
  • 7
  • 1
    Thank you so much for the explanation; it was very insightful. Regarding @Viet 's solution, `itemQuerySnapshot` isn't itself an array. With that, do have any other suggestions? – bob marsh Aug 02 '21 at 04:32
  • Just added an quick fix for it, convert it to array and then update the state – sasal Aug 02 '21 at 04:33
0

Instead of running it for each value maybe you should try to map it as a list and update the state once

function addItemsToList(newItems) {
  const updatedList = [
    ...items,
    ...newItems
  ]

  return updatedList;
} 

useEffect(() => {
  firestore.collection('items').where('uid', '==', currentUser.uid).get().then((itemSnapshot) => {
    const docs = itemSnapshot.map((doc) => ({
       data: doc.data(),
       ref: doc.ref
    });
    setItems(addItemsToList(docs));
  })
  setLoading(false);
}, []);
EdwynZN
  • 4,895
  • 2
  • 12
  • 15
0

You are using spread operator in a wrong way! This way will not update the array.

You can do that by using es6 spread to concat multiple arrays something like below:

const updatedList = [
    ...items,
    ...[{ data: i.data(), ref: i.ref }]
  ]

To know more: Using es6 spread to concat multiple arrays

Rashed Rahat
  • 2,357
  • 2
  • 18
  • 38