Search code examples
javascriptreact-nativegoogle-cloud-firestorereal-timertk-query

How can I unsubscribe from Firebase onSnapShot in an RTK Query?


I'm creating an app in React Native that depends on realtime updates. When a user updates the data in their session, another user needs to immediately see that update without refreshing their app. I'm using RTK Query to manage my store, but can't figure out how to close the memory leak if I use onSnapShot in my query. Is there another Redux solution I need to consider?

I've tried passing the data through props to manage the data but the management becomes a little complicated when using complex components.

I start with the following in the component but want to move it to the api:


export const teamPlayersApi = createApi({
  reducerPath: "teamPlayers",
  baseQuery: fakeBaseQuery(),
  tagTypes: ["TeamPlayer"],
  endpoints: (builder) => ({
    fetchTeamPlayersByMatchId: builder.query({
      async queryFn(matchId) {
        let players = [];
        return new Promise((resolve, reject) => {
          const playersRef = query(
            collection(db, "teamPlayers"),
            where("playerMatchId", "==", matchId)
          );
          //real time update

          onSnapshot(playersRef, (snapshot) => {
            players = snapshot.docs.map((doc) => ({
              id: doc.id,
              player: doc.data()
            }));
            resolve({ data: players });
          });
        });
      }
    })
  })
})

UPDATE: @phry The below code works, but I don't see a re-render happening when the 'draft' is changed. What fires off a re-render?

const {
    data: fetchPlayersData,
    error: fetchPlayersError,
    isFetching: isFetchingPlayers
  } = useFetchTeamPlayersStreambyMatchIdQuery(matchId);

  useEffect(() => {
    console.log("players changed");
  }, [fetchPlayersData]);

...AND IN THE API

fetchTeamPlayersStreambyMatchId: builder.query({
      async queryFn(matchId) {
        try {
          const teamPlayersRef = collection(db, "teamPlayers");
          const tpq = query(
            teamPlayersRef,
            where("playerMatchId", "==", matchId)
          );
          const querySnap = await getDocs(tpq);

          let players = [];

          querySnap.forEach((doc) => {
            return players.push({
              id: doc.id,
              player: doc.data()
            });
          });

          return { data: players };
        } catch (error) {
          console.error(error.message);
          return { error: error.message };
        }
      },
      async onCacheEntryAdded(
        matchId,
        { updateCachedData, cacheDataLoaded, cacheEntryRemoved }
      ) {
        let unsubscribe = () => {};
        try {
          await cacheDataLoaded;
          const playersRef = query(
            collection(db, "teamPlayers"),
            where("playerMatchId", "==", matchId)
          );
          unsubscribe = onSnapshot(playersRef, (snapshot) => {
            players = snapshot.docs.map((doc) => ({
              id: doc.id,
              player: doc.data()
            }));

            updateCachedData((draft) => {
              draft = [];
              draft.push(players);
            });
          });
        } catch {}
        await cacheEntryRemoved;
        unsubscribe();
      }
    })

Solution

  • This has two different parts:

    • a queryFn getting the initial data using onValue. This is the point where your query enters a loading state and finishes at some point with a first value.
    • a onCacheEntryAdded lifecycle function that calls onSnapshot, updates values and holds the subscription. This is the point where a subscription is generated and your data is updated with new values while your component uses the cache entry.
    export const teamPlayersApi = createApi({
      reducerPath: "teamPlayers",
      baseQuery: fakeBaseQuery(),
      tagTypes: ["TeamPlayer"],
      endpoints: (builder) => ({
        fetchTeamPlayersByMatchId: builder.query({
          async queryFn(matchId) {
            let players = [];
            return {
              data: await new Promise((resolve, reject) => {
                const playersRef = query(
                  collection(db, "teamPlayers"),
                  where("playerMatchId", "==", matchId)
                );
                // probably more logic here to get your final shape
                onValue(
                  playersRef,
                  (snapshot) => resolve(snapshot.toJSON()),
                  reject
                );
              }),
            };
          },
          async onCacheEntryAdded(
            matchId,
            { updateCachedData, cacheDataLoaded, cacheEntryRemoved }
          ) {
            let unsubscribe = () => {};
            try {
              await cacheDataLoaded;
              const playersRef = query(
                collection(db, "teamPlayers"),
                where("playerMatchId", "==", matchId)
              );
              unsubscribe = onSnapshot(playersRef, (snapshot) => {
                players = snapshot.docs.map((doc) => ({
                  id: doc.id,
                  player: doc.data(),
                }));
    
                updateCachedData((draft) => {
                  // or whatever you want to do
                  draft.push(players);
                });
              });
            } catch {}
            await cacheEntryRemoved;
            unsubscribe();
          },
        }),
      }),
    });