2

I am trying to return the value from function that has the onSnapshot() event but keep getting this weird error. Basically, I call this action and return the data from it like I would in any other function. But I keep getting this error and I do not know how to fix it.

This is the error

Uncaught TypeError: Cannot add property 0, object is not extensible
    at Array.push (<anonymous>)

This the function

export const getQuestions = () => {
  var questions = [];
  onSnapshot(collection(firebaseDatabase, "questions"), (querySnapshot) => {
    querySnapshot.docs.forEach((doc) => {
      if (doc.data() !== null) {
        questions.push(doc.data());
      }
    });
  });
  return questions;
};

Also this function is used with Redux Thunk and Redux Toolkit.

import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import { getQuestions } from "../../utils/firebase-functions/firebase-functions";

export const getAllQuestions = createAsyncThunk(
  "allQuestions/getAllQuestions",
  async () => {
    const response = getQuestions();
    return response;
  }
);

export const allQuestionsSlice = createSlice({
  name: "allQuestions",
  initialState: {
    allQuestions: [],
    loading: false,
    error: null,
  },
  extraReducers: {
    [getAllQuestions.pending]: (state) => {
      state.loading = true;
      state.error = null;
    },
    [getAllQuestions.fulfilled]: (state, action) => {
      state.allQuestions = action.payload;
      state.loading = false;
      state.error = null;
    },
    [getAllQuestions.rejected]: (state, action) => {
      state.loading = false;
      state.error = action.payload;
    },
  },
});

export default allQuestionsSlice.reducer;

Where it is dispatched

const dispatch = useDispatch();
  const tabContentData = useSelector(
    (state) => state.allQuestions.allQuestions
  );

  useEffect(() => {
    dispatch(getAllQuestions());
  }, [dispatch]);

  console.log(tabContentData);
Dharmaraj
  • 47,845
  • 8
  • 52
  • 84
Samke11
  • 385
  • 5
  • 17
  • Are you trying to get data only once or want to listen to realtime updates? Also you don't need to check if `doc.data()` is `null` or no in case of query snapshots. – Dharmaraj Mar 26 '22 at 16:48
  • I want to listen to realtime updates. This function is going to be used inside the useEffect() – Samke11 Mar 26 '22 at 16:49
  • 1
    Can you share your code that includes `useEffect()` and also where you are showing this questions? You can just update these questions in state directly perhaps. – Dharmaraj Mar 26 '22 at 16:50
  • I am using it with Redux-Thunk but I will show it – Samke11 Mar 26 '22 at 16:51
  • 1
    Data is loaded from Firestore (and most cloud APIs) asynchronously, since it needs to come from the internet and may take time. To prevent blocking the app, your main code (including your `return questions`) continues while this data is being loaded, and then when the data is available the callback you passed to `onSnapshot` is called (and then runs your `questions.push(doc.data())`). So your code returns the questions before they have loaded. – Frank van Puffelen Mar 26 '22 at 17:01
  • Thank you for clear explanation. Is there any function from the Firebase that I could use to check if the data is loaded? – Samke11 Mar 26 '22 at 17:03

1 Answers1

2

You can try returning a promise when the data is being fetch for first time as shown below:

let dataFetched = false; 

export const getQuestions = () => {
  return new Promise((resolve, reject) => {
    onSnapshot(collection(firebaseDatabase, "questions"), (querySnapshot) => {
      querySnapshot.docs.forEach((doc) => {
        if (doc.data() !== null) {
          questions.push(doc.data());
        }
      });
 
      if (!dataFetched) {
        // data was fetched first time, return all questions
        const questions = querySnapshot.docs.map(q => ({ id: q.id, ...q.data()}))
        resolve(questions)
        dataFetched = true;
      } else {
        // Questions already fetched,
        // TODO: Update state with updates received
      }
    }); 
  })
};

getQuestions() now returns a Promise so add an await here:

const response = await getQuestions();

For updates received later, you'll have to update them directly in your state.

Dharmaraj
  • 47,845
  • 8
  • 52
  • 84
  • 1
    Thank you very much! I now understand why it did not work. This code was easy to understand – Samke11 Mar 26 '22 at 17:04
  • @Dharamaj. Could you just explain what do you mean updates received later? Do you know any way to trigger the onSnapshot event in useEffect()? – Samke11 Mar 26 '22 at 17:09
  • @Samke11 so whenever you add/update/remove a document, the function in `onSnapshot()` will trigger. If you add a document in Firestore, you would want to update the already fetch questions array right? The promise has resolved when the data was fetched first time, now you would have to update the array in state directly with updated question. yes you can use onSnapshot directly in useEffect, and keep updating state whenever an update is received. Checkout [this answer](https://stackoverflow.com/a/71036396/13130697) for an example. – Dharmaraj Mar 26 '22 at 17:12
  • Thank you again for answering. To be honest how would I update the array in state directly? How is that done? Sorry if this is a stupid question – Samke11 Mar 26 '22 at 17:15
  • @Samke11 if you see the linked answer in my previous comment, that would be the simplest example. Maybe you can add and dispatch another action to update that in state . I am not totally sure about Redux in this case, it might best to post another question specifically for that with details about your Redux setup. – Dharmaraj Mar 26 '22 at 17:18