'Redux Toolkit: can I use createAsycThunk with Firebase listener functions (e.g. firestore.collection.onSnapshot)

Is there a way to use createAsyncThunk with Firebase listeners, for example firestore.collection.onSnapshot?

It may not work because the way onSnapshot works (it's a listener that receives data and fires a callback every time firestore updates and it returns a function that unsubscribes the listener). I tried implementing createAsyncThunk but couldn't figure it out.

Here's my current thunk implementation which does work:

const listenerUnsubscribeList = [];
export function fetchProjects() {
  return dispatch => {
    dispatch(fetchProjectsPending());
    const unsubscribe = firestore
      .collection('projects')
      .onSnapshot(
        snapshot => { dispatch(fetchProjectsFulfilled(snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })))) },
        error => { dispatch(fetchProjectsError(error)) },
      );
    listenerUnsubscribeList.push(unsubscribe);
  }
}

Here's my attempt at createAsyncThunk which does not work. I'm getting database/fetchProjects/pending and database/fetchProjects/fulfilled but payload is undefined

const listenerUnsubscribeList = [];
export const fetchProjects = createAsyncThunk(
  'database/fetchProjects',
  async (_, thunkAPI) => {
    const unsubscribe = await firestore
      .collection('projects')
      .onSnapshot(
        snapshot => { return snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })) },
        error => { return error },
      );
    listenerUnsubscribeList.push(unsubscribe);
  }
);


Solution 1:[1]

I tried making it work with createAsyncThunk but did not manage to solve it.

I dont know if my approach is the right one but I simply created a custom thunk that handles re-firing any action as needed. I create a loading / fulfilled / error action that will handle the data accordingly.

// Action.js

/*
  listenToInboxInDb is a firebase listener that will re-fire with fresh data.
  See how I am returning unsubscribe to be able to call unsubscribe 
  when my component unmounts
*/

export const getConnectionRequests = () => {
  return (dispatch) => {
    dispatch(fetchConnectionsRequestsLoading())

    const unsubscribe = listenToInboxInDb(([data, error]) => {
      if (data) {
        dispatch(fetchConnectionsRequestFullfilled(data))
      }

      if (error) {
        dispatch(fetchConnectionsRequestRejected(error))
      }
    })

    return unsubscribe
  }
}
// Reducer.js

const connectionsSlice = createSlice({
  name: "connections",
  initialState: INITIAL_STATE,
  reducers: {
    // ...other reducers

    fetchConnectionsRequestsLoading(state) {
      state.connectionRequestsStatus = REQUEST_STATUS.loading
    },
    fetchConnectionsRequestFullfilled(state, action) {
      state.error = null
      state.data.connectionRequests = action.payload
      state.connectionRequestsStatus = REQUEST_STATUS.succeeded
    },
    fetchConnectionsRequestRejected(state, action) {
      state.error = action.error
      state.connectionRequestsStatus = REQUEST_STATUS.failed
    },
  },
})

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Walter Monecke