5

A main selling point of Firestore is the ability to use it as a online/offline source of truth. I'm using it in this way right now: updating the Firestore document directly on an action, then listening to Firestore DB changes and mapping this back to local state. However, relying on this latency compensation and mapping back to local state is not sufficient for quick updates (taps, toggles even with a small document size). For example, toggles will "jitter" as the RN toggle presumptively shifts on tap, and the local state hasn't been updated until it already returns see video example. It appears worse on Android and the problem isn't strictly limited to basic toggles.

  1. Does document size or query result size have a bigger impact on latency compensation? Our document size is very small right now with a worst case ~1000 query result set. We could make the documents 1000x bigger (100kb) and have a query result set of size 1. Update: Testing appears inconsistent here, latency compensation is not ideal in either case
  2. Which of the following other things may impact latency compensation?

    • Using queries with custom indexes. Note: we're not currently reading from cache, we're using the JS SDK
    • Multiple writes. Would multiple writes to the same document make it worse (4 quick writes vs. 2 quick writes). Update: not clear this makes a big difference.
    • Using the native vs. JS module. We're currently using the Firestore Web SDK with an Expo app. Update: switching to native module via React-Native Firestore has no apparent performance improvement.
  3. Is it common for people to build a local data shim layer / local app state with React Native / Firestore apps to help improve local performance speed? Are there any suggested libraries for this?

On app load, mount the listener, and export the result to context to be used through the app

const [user, setUser] = useState();

firebase.firestore().collection(`users/${user.uid}`).onSnapshot(qs => setUser(oldState => {
    const newState = {};
    qs.docChanges().forEach(change => {
      if (change.type === "added" || change.type === "modified") {
        newState[change.doc.id] = {
          docid: change.doc.id,
          ...change.doc.data(),
        };
      } else if (change.type === "removed") {
        delete oldState[change.doc.id];
      }
    });

    return {
      ...oldState,
      ...newState,
    };
  }))

Sample component and function to toggle notifications: (switch is jittery)

const toggleNotifications = (user, value) => {
  firebase.firestore().doc(`users/${user.uid}`).update({
    wantNotifications: value,
  });
};

const TestComponent = () => {
  //gets from context, set in listener mounted on app load
  const { user } = useUserContext();
  return (
    <Switch
      value={user.wantNotifications}
      onValueChange={value => toggleNotifications(user, value)}
    />
  );
};
learningAngular
  • 231
  • 3
  • 14
  • I recommend using [react-redux-firebase](http://react-redux-firebase.com/docs/getting_started) to handle async communication with firebase and firestore. – Makan Apr 16 '20 at 15:53
  • @Makan thanks! The automatic binding/unbinding of listeners and mapping collections to local state is nice but not sure it answers this question directly. I think the state it react-redux-firebase provides still relies on updating the firestore DB to receive updates. Is my understanding wrong? – learningAngular Apr 16 '20 at 16:32
  • 2
    You are right react-redux-firestore also receives update from firestore but what I am trying to say is taking advantage of the pattern provided by redux to keep states update almost synchronously. In this pattern updating firestore would be through async dispatch actions which eventually update states. However, it gives us an opportunity to update local state before dispatching action, so react components could rely on local states which always are sync with firestore. – Makan Apr 16 '20 at 18:26

2 Answers2

0

This not an answer, just a long comment :)

@learningAngular For example, in toggleNotifications just need to call an async action creator and don't worry about putting any logic inside react component.

Instead Redux pattern gives space to do some logics, in this case because user's last moment decision is source of truth so dispatch function would always set a local tempState and updatingStatus before start updating firestore, then after firestore promise either resolved or rejected dispatches an action to reducer to reset updatingStatus. Then a selector would check if updatingStatus is true to just rely on local tempState otherwise rely on listened firestore state. Finally, react component use selector result as currently valid state.

I am not answering this question because I don't have that much experience. I am also curious if a good solution is out there, but this is what I think is the solution at this moment.

Makan
  • 641
  • 8
  • 13
0

I updated the answer with specific learnings, but after a lot of testing, my biggest general learnings so far are

  • Latency compensation can be very inconsistent even with the same data and environment. Listeners can take time to "warm up", as is mentioned in other questions. It is hard to have a standard metric here.
  • Document size DOES impact latency compensation. Everything else so far is inconclusive.
learningAngular
  • 231
  • 3
  • 14