h1kiga
h1kiga

Reputation: 84

How to avoid firing multiple redux actions with real time firestore listeners

Introduction

A Little Warning: I do use Redux Toolkit

I have bunch of lists, one of which should be active. And depending on some context, active list should be different. For example I have 3 lists (A, B, C) and let's look at following patterns:

  1. List B is active and I decided to create a new list. After creating list D, list D should be active:
    • List D - active
    • List C
    • List B
    • List A
  2. List B is active and I decided to change the page. When I come back, List B should be active as it was before changing the page.

The problem

As I initiate the setListsAction from the beginning, it always listens to the firestore and gets invoked every time I manipulate with the store (add, remove, update) and then pass all the data to the reducer. For this reason, I can't control which action was actually performed. For this case in my setListsReducer I check if there's already an active list, if so, I don't change it (covering my second pattern in the examples section). However, with such logic I can't set newly created list as active, because there'll be always an active list that's why in my createListAction I pas a newly created list to the payload and in createListReducer I set payload as the active list. However, the caveat of this approach is that both setListsAction and createListAction gets triggered, so redux state gets updated two times in a row, making my components rerender unnecessary. The cycle looks like that:


My Code

Actions

setListsAction

export const subscribeListsAction = () => {
  return async (dispatch) => {
    dispatch(fetchLoadingActions.pending());
    const collection = await db.collection('lists');
    const unsubscribe = collection
      .onSnapshot((querySnapshot) => {
        const lists = querySnapshot.docs.map((doc) => {
          const list = { ...doc.data(), id: doc.id };
          return list;
        });

        dispatch(
          fetchLoadingActions.fulfilled({
            lists,
          })
        );
      });
  };
};

createListAction

export const createListActionAsync = (list) => {
  return async (dispatch: Dispatch<PayloadAction<any>>) => {
    dispatch(listsLoadingActions.pending());
    const docList = await db.collection('lists').add(list);
    const fbList = { ...list, id: docList.id };
    dispatch(listsLoadingActions.fulfilled(fbList));
  };
};

Reducers

setListsReducer

builder.addCase(fetchLoadingActions.fulfilled, (state, { payload }) => {
      state.lists = payload.lists;
      const activeList = state.activeList
        ? payload.lists.find((l) => l.id === state.activeList.id)
        : payload.lists[0];

      state.activeList = activeList;
    });

createListReducer

builder.addCase(listsLoadingActions.fulfilled, (state, { payload }) => {
      state.activeList = payload;
    });

Sum

So I would like you to propose a better way to handle my problem. I tried to solve it, using change type on docChanges but when I init setListsAction, all docs' changes are type of added and workarounds may damage further implementations of the app. Probably, I need to give up real time database and use get method instead.

Upvotes: 0

Views: 293

Answers (1)

samthecodingman
samthecodingman

Reputation: 26276

If you eliminate the createListReducer and listLoadingActions, you should be able to do everything from inside the ListsAction hook. Using await db.collection('lists').add(list) should refire the listener on the lists collection once it's been added to the database successfully.

export const subscribeListsAction = () => {
  return async (dispatch) => {
    dispatch(fetchLoadingActions.pending());
    const collection = db.collection('lists'); // no need to await?
    let firstLoad = true; // used to determine whether to use docs or docsChanges
    const unsubscribe = collection
      .onSnapshot((querySnapshot) => {
        if (firstLoad) {
          const lists = querySnapshot.docs.map((doc) => ({ ...doc.data(), id: doc.id }));
          firstLoad = false;
          // Get and set initial active list?
          dispatch(
            fetchLoadingActions.fulfilled({
              lists,
            })
          );
        } else {
          // optionally fire dispatch(fetchLoadingActions.pending()) again?
          const listsCopy = [...state.lists]; // copy the existing list to mutate it
          let activeList = state.activeList; // store the current activeList
          querySnapshot.docChanges().map((change) => {
            if (change.type === "added") {
              const thisList = { ...change.doc.data(), id: change.doc.id };
              listsCopy.splice(change.newIndex, 0, thisList);
              activeList = thisList;
            } else if (change.type === "modified") {
              listsCopy.splice(change.oldIndex, 1);
              listsCopy.splice(change.newIndex, 0, { ...change.doc.data(), id: change.doc.id });
            } else if (change.type === "removed") {
              listsCopy.splice(change.oldIndex, 1);
              if (activeList.id === change.doc.id) {
                // the current active list was removed!
                activeList = undefined;
              }
            }
          });
          dispatch(
            fetchLoadingActions.fulfilled({
              lists: listsCopy,
              activeList: activeList || listsCopy[0] // use activeList or fallback to first list in listsCopy, could still be undefined if listsCopy is empty!
            })
          );
        }
      });
    return unsubscribe;
  };
};

Regarding the active list history, you could either use the URL ?list=some-id to store the selected list with the History API or you could store an array called activeListHistory in your state variable where you push() and pop() to it as necessary (make sure to handle cases where the old list no longer exists and where there are no entries in the array).

Upvotes: 4

Related Questions