DEV Community

Cover image for 🚂 A Cloud Traveler’s Side Quest: Debugging Firestore events
Patti Shin 💫
Patti Shin 💫

Posted on • Updated on

🚂 A Cloud Traveler’s Side Quest: Debugging Firestore events

Series: 🚂 Journey to Cloud City!
Episode: 0
Characters: Cloud Traveler, a new engineer learning about Cloud
Tools: Redux, Cloud Firestore, Google Cloud Platform

Welcome to episode 0. We’re engineers, we start counting from 0 not 1.
I wanted to experiment following a character, Cloud Traveler, getting into the weeds and debugging these everyday sort of issues using known cloud and frontend tools.

Issue: When a Firestore onSnapshot function is declared within a Redux action (createActionThunk()), connected Redux reducers do not “seem” to be getting called when Firestore collection emits a change event.

A solution: Redux actions should always return a value per the one-way data flow model and any listener function should be initialized once in an application. Create a new getter action that will retrieve the new state of the collection rather than relying on the change values given in the onSnapshot callback function.

Please note, this is a nice hack for smaller projects. If you want to build event-driven architecture with Cloud Firestore events, recommend that you take a look at Google Cloud’s EventArc.


Cloud Traveler is annoyed.

Getting their first web development project to track how much sunlight their plants are receiving was relatively frictionless leveraging Firebase and Cloud Firestore. However, getting reliable and deterministic event data from this new collection has been frustrating. The Redux action, pingSunlightLevel is meant to send relevant change data information up to the reducer, plantSlice to the main store data. However, it's not working as expected.


// Actions
export const pingSunlightLevel = createAsyncThunk ('pingSunlightLevel', async () => {
  const ref = firebaseInstance.db.collection('plant_sunlight');
  await ref.onSnapshot((snapshot) => {
    snapshot.docChanges().forEach((change) => ({ change });
  });
)};

// Reducer
const plantSlice = createSlice({ 
  name: 'plants',          
 . . .
  extraReducers: (builder) => {
    builder
      .addCase(pingSunlightLevel.fulfilled, (state, action) => {
        state.plantLightLevel = action?.payload?.change;
        return state;
      })
  . . .
  },
});

Enter fullscreen mode Exit fullscreen mode

"Time to change my perspective" sighs Cloud Traveler, as they put on their Sherlock Holmes cap. “Let me think about what I know of the Redux one way data flow and leverage some debugging statements in that snapshot callback function."

The snapshot is receiving the expected Firestore events when the collection is updated. But, for some reason the reducer isn't responding.

Lo and behold, they find that the snapshot was receiving the expected Firestore events when the plant_sunlight collection was updated. Bad news was that the culprit seems to be that the reducer was somehow not responding.


// Actions
export const pingSunlightLevel = createAsyncThunk ('pingSunlightLevel', async () => {
  const ref = firebaseInstance.db.collection('plant_sunlight');
  await ref.onSnapshot((snapshot) => {
    snapshot.docChanges().forEach((change) => {
      console.log(change);  // Getting changes
      debugger
      return { change };
    }); 
  });
)};

// Reducer
const plantSlice = createSlice({ 
  name: 'plants',          
 . . .
  extraReducers: (builder) => {
    builder.addCase(pingSunlightLevel.fulfilled, (state, action) => {
     console.log(action.payload); // Unresponsive 
     state.plantLightLevel = action?.payload?.change;
     return state;
  });
  . . .
  },
});
Enter fullscreen mode Exit fullscreen mode

Cloud Traveler furrows their brow.

“I need to figure out why the reducer isn't responding. I'll go through the possibilities one at a time to narrow down the issue.

Was the connected UI button triggering the correct action?
✅ Check

Was the reducer connected to the main Redux store correctly?
✅ Check.

Was the reducer referencing the right Redux action function?
✅ Sure looks like it.

What happens if I comment out that snapshot function, would the reducer respond to the action if I pass return just a dummy value?
🤨 Oh ..."

The reducer, at this point, did get called.
The two pieces Cloud Traveler had investigated,
the snapshot function and the Redux flow, seemed to be misaligned.

Cloud Traveler’s pingSunlightLevel Redux action was, in fact, not returning a value as it should per the one-way data flow model when the snapshot function was declared within it. Instead, it was creating additional instances of the listener inside their application on every action call!

Seeing a path forward, Cloud Traveler refactors out the snapshot function pingSunlightLevel to only be declared once and updates the action function, now getPlantSunlightLevel, to just retrieve the current state of plant_sunlight collection.

THE FINAL VIEW:


// Actions
// Newly created action that gets called by pingSunlightLevel
export const getPlantSunlightLevel = createAsyncThunk ('getPlantSunlightLevel', async () => {
  const ref = firebaseInstance.db.collection('plant_sunlight');
  const currentState = await ref.get().then((querySnapshot) => {
    let list = [];
    querySnapshot.docs.forEach(doc => list.push(doc.data()));
    return list;
  });
  return { currentState };
});

// Previous action, now just a Firestore listener
export const pingSunlightLevel = async (dispatch) => {
  const ref = firebaseInstance.db.collection("plant_sunlight");
  await ref.onSnapshot((snapshot) => {
    snapshot.docChanges().forEach(() => dispatch?.(getPlantSunlightLevel()));
  });
};

// Reducer
const plantSlice = createSlice({ 
  name: 'plants',          
 . . .
  extraReducers: (builder) => {
    builder
      .addCase(pingSunlightLevel.fulfilled, (state, action) => {
        state.plantLightLevel = action?.payload?.currentState;
        return state;
      })
  . . .
  },
});

Enter fullscreen mode Exit fullscreen mode

Finally at ease, our hero, Cloud Traveler can now observe real-time sunlight levels in their new plant web application.

FIN

Top comments (0)