Optimistic Updates

With 800 ms of latency, a heart that fills only after the server replies feels broken. An optimistic update changes the cache at once, assuming success, and undoes the change if the server refuses:

src/app/book/[id].tsx: updating the cache before the request, and rolling back (excerpt)CSS
    onMutate: async () => {                          // runs before the request is sent
      await qc.cancelQueries({ queryKey: keys.book(id) });
      const before = qc.getQueryData<ApiBook>(keys.book(id));
      qc.setQueryData<ApiBook>(keys.book(id), (b) => b && { ...b, favorite: !b.favorite });
      console.log('optimistic: favorite =', !before?.favorite);
      return { before };                             // becomes `context` below
    },
    onError: (err, _vars, context) => {
      console.log('rolled back:', err.message);
      qc.setQueryData(keys.book(id), context?.before);
    },

cancelQueries stops a running refetch that would overwrite the optimistic value. The practice server's POST /fail-next makes the next favorite request answer 503. Tapping the heart twice, arming that hook between:

Output of 164
'optimistic: favorite =', true
'book 3: data, fetch idle,', 'updated 16:16:19'
...
'optimistic: favorite =', false
'book 3: data, fetch idle,', 'updated 16:16:28'
'rolled back:', 'POST /books/3/favorite failed: 503'
'book 3: data, fetch fetching,', 'updated 16:16:29'
'book 3: data, fetch idle,', 'updated 16:16:30'

Each tap re-rendered the screen immediately with the new heart; the request followed. The second one failed, so onError put back the saved copy and onSettled refetched to be sure. Use optimistic updates for small, easily reversed changes (favorites, toggles, reordering), and tell the user when you roll back, for example with a toast (ToastAndroid). For payments, wait for the server and show a spinner.