reduxjs / redux-toolkit

The official, opinionated, batteries-included toolset for efficient Redux development
https://redux-toolkit.js.org
MIT License
10.72k stars 1.17k forks source link

Using Redux RTK with optimistic updates #2341

Closed wmonecke closed 2 years ago

wmonecke commented 2 years ago

Hey there!

I am trying to use Redux RTK with Firebase Firestore. Firestore already handles requests optimistically in the sense that we shouldn't be waiting for the request to end to do something else because Firestore will retry the request many times in the background or when the connection resumes.

The problem I am having is how to add an item to the cache without retriggering a fetch.

My current code:

import { getFoldersAndTagsFromDb, saveFolderIntoDb } from "../../api"
import { createApi, fakeBaseQuery } from "@reduxjs/toolkit/query/react"
import { saveTagIntoDb } from "../../api/tags"

export const TAGS_AND_FOLDERS_ITEM_TYPES = {
  tag: "tag",
  folder: "folder",
}

export const ProjectsApiSlice = createApi({
  reducerPath: "projectsApi",
  baseQuery: fakeBaseQuery(),
  tagTypes: ["TagsAndFolders"],
  endpoints: (builder) => ({
    fetchTagsAndFoldersFromServer: builder.query({
      async queryFn() {
        const [data, error] = await getFoldersAndTagsFromDb()
        if (error) return { error }
        return { data }
      },
      keepUnusedDataFor: 180, // 3mins
      providesTags: ["TagsAndFolders"],
    }),
    createTagOrFolder: builder.mutation({
      async queryFn({ item, itemType = TAGS_AND_FOLDERS_ITEM_TYPES.tag }) {
        console.log("createTagOrFolder API SLICE - item:", item, itemType)
        if (itemType === TAGS_AND_FOLDERS_ITEM_TYPES.tag) {
          const [data, error] = await saveTagIntoDb(item)
          if (error) return { error }
          return { data }
        }
        const [data, error] = await saveFolderIntoDb(item)
        if (error) return { error }
        return { data }
      },

    }),
  }),
})

export const {
  useCreateTagOrFolderMutation,
  useFetchTagsAndFoldersFromServerQuery,
} = ProjectsApiSlice

When creating a new Tag or Folder I would like to instantly add it to the cache and it be shown in the UI and let Firestore handle the request retries and the execution in general.

I am able to fetch Tags and Folder, also able to create them. I am just missing the way on how to add a newly created item to the cache without invalidating it.

Any guidance is welcome!

phryneas commented 2 years ago

Adding completely new cache entries is not possible yet - but adding things to existing cache entries is: Optimistic updates.

Just out of couriosity: have you also taken a look at redux-firebase and what made you choose RTK Query? At a glance it seemed to be pretty optimized for firebase.

ghost commented 2 years ago

@phryneas Awesome thanks! I just wanted to loosely couple my crud methods to redux RTK in case that I want to change my DB in the near future. redux-firebase is nice but it would involve more refactoring I guess.

wmonecke commented 2 years ago

@phryneas Do you know what I might be doing wrong here? I am able to create a tag but it isn't affecting the cache. I thought mutating the draft would update the cache just like a slice would to an incoming state proxy.

export const ProjectsApiSlice = createApi({
  reducerPath: "projectsApi",
  baseQuery: fakeBaseQuery(),
  tagTypes: ["TagsAndFolders"],
  endpoints: (builder) => ({
    fetchTagsAndFoldersFromServer: builder.query({
      async queryFn() {
        const [data, error] = await getFoldersAndTagsFromDb()
        if (error) return { error }
        return { data }
      },
      keepUnusedDataFor: 180, // 3mins
      providesTags: [{ type: "TagsAndFolders" }],
    }),
    createTagOrFolder: builder.mutation({
      async queryFn({ item, itemType = TAGS_AND_FOLDERS_ITEM_TYPES.tag }) {
        if (itemType === TAGS_AND_FOLDERS_ITEM_TYPES.tag) {
          const [data, error] = await saveTagIntoDb(item)
          if (error) return { error }
          return { data }
        }
        const [data, error] = await saveFolderIntoDb(item)
        if (error) return { error }
        return { data }
      },
      async onQueryStarted(payload, { dispatch, queryFulfilled }) {
        console.log("createTagOrFolder -> onQueryStarted", payload)
        const patchRecipe = ProjectsApiSlice.util.updateQueryData(
          "fetchTagsAndFoldersFromServer",
          undefined,
          (draft) => {
            if (payload.type === "tag") {
              return {
                ...draft,
                tags: [...draft.tags, payload.item],
              }
            } 

            return {
              ...draft,
              folders: [...draft.folders, payload.item],
            }
          }
        )
        const cachePatch = dispatch(patchRecipe)
        queryFulfilled.catch(cachePatch.undo)
      },
    }),
...

It works if I add: invalidatesTags: [{ type: "TagsAndFolders" }],

But I do not want to re-fetch data. To me adding invalidatesTags is a half measure. What if I am very sure the request will go through (i.e. Firestore manages the retries for me) and I just want to add an item to cache without forcing a re-fetch?