Search code examples
javascriptreduxgoogle-cloud-firestorereact-reduxredux-thunk

Firebase firestore : Unhandled Rejection (RangeError): Maximum call stack size exceeded


Started playing around with createAsyncThunk for learning purpose, decided to implement a shopping cart with firebase firestore but I ran into problems when trying to implement pagination in my react app.

How should I return the last visible state into my redux state during the initial load and subsequent load (infinite loading)

I am basing on code from redux tutorial sandbox :https://codesandbox.io/s/github/reduxjs/redux-essentials-example-app/tree/checkpoint-3-postRequests/?from-embed, but instead of connecting to a fake api, I am using firebase firestore.

Code to fetch product from firestore : ProductSlice.js

const InitialState = {
   products : [],
   status: 'idle',
   error: null,
   last: null, //to hold lastVisible when fetching data from firestore
}

export const fetchProducts = createAsyncThunk(types.RECEIVE_PRODUCTS, async (limit) => {
   const resp = await fire_base_product.firestore()
      .collection(collection_name).orderBy('id').limit(limit)

   let result = resp.get().then((querySnapshot) => {
      const lastVisible = querySnapshot.docs[querySnapshot.docs.length - 1] //how set this to redux state

      const products = querySnapshot.docs.map((doc)=> {
         return { ...doc.data()}
      })

      return { 
         products: products, 
         lastVisible: lastVisible
      };
   })

   return result;
}

I am not quite sure on how to set this lastVisible data back into redux state, is it possible to do that with reference?

#Edit:

Tried to return both product list and last visible as an array and assign lastVisible in createSlice as stated below:

const productSlice = createSlice({
   name:'products',
   initialState: 
   reducers: {},
   extraReducers:{
      [fetchProducts.fulfilled]: (state, action) => {
         state.products = state.products.concat(action.payload.products)
         state.last = action.payload.lastVisible // this causes call stack error
      }

   }
});

With the above coding, two error will be reported if I run react app,

  1. Trying to assign non serialize value into redux state error message: non-serialize value

  2. Maximum call stack size exceeded in firestore call stack size exceeded

I then tried to add middleware serializableCheck during create configuration as below:

export default configureStore({
   middlleware: getDefaultMiddlleWare({
      serializableCheck: {
         //ignore action type
         ignoredActions : ['RECEIVE_PRODUCTS/fulfilled']
         // ignore path
         ignoredPath: ['products.last']
      }
   }),
   ... // reducer codes
})

Even though now I have dismissed the first error, call stack exceeded still exists. Does anyone knows why this is happening ? Feel free to discuss if there is any workaround on this. Thanks.

Edit 2

Similar approach works when using context but does not work when using redux. Do I need to wrap return in promise as suggested in Firebase Unhandled error RangeError: Maximum call stack size exceeded ?


Solution

  • Did not managed to find a way to save lastVisible, but I found a workaround by just keeping track of the last retrieve id of my firestore data by saving it into redux state.

    export const fetchProducts = createAsyncThunk(types.RECEIVE_PRODUCTS, async (limit) => {
       const resp = await fire_base_product.firestore()
          .collection(collection_name).orderBy('id').limit(limit)
    
       let result = resp.get().then((querySnapshot) => {
          var lastVisible = limit - 1; //only keep track of ID so we can avoid saving un-serialize coded
    
          const products = querySnapshot.docs.map((doc)=> {
             return { ...doc.data()}
          })
    
          return { 
             products: products, 
             lastVisible: lastVisible
          };
       })
    
       return result;
    }
    

    And when during fetch of additional data we can then access the state by using getState() as below:

    export const fetchMoreProducts = createAsyncThunk(types.LOAD_MORE_PRODUCTS, async (limit, {getState}) => {
        const last = getState().products.last
    
        var newProducts = await firebase_product.firestore()
            .collection('store_products').orderBy('id')
            .startAfter(last).limit(limit)
    
        const result = newProducts.get().then((querySnapshot) => {
                var lastVisible = last + limit;
    
                const products = querySnapshot.docs.map((doc) => {
                    return { ...doc.data() }
                })
    
                return {
                    products : products, 
                    lastVisible: lastVisible
                }
            })
    
        // return retrieved data from firebase 
        return result
    })
    

    But doing this, I could skip the serialization check config all together as well. Not sure if this is the correct way, but this is how I got pagination working. Feel free to let me know if there is other way to approach this.