Skip to content

Add helpers for optimistic updates #5073

@markerikson

Description

@markerikson

Right now the standard pattern for an optimistic update is pretty verbose:

const api = createApi({
  baseQuery: fetchBaseQuery({
    baseUrl: '/',
  }),
  tagTypes: ['Post'],
  endpoints: (build) => ({
    getPost: build.query<Post, number>({
      query: (id) => `post/${id}`,
      providesTags: ['Post'],
    }),
    updatePost: build.mutation<void, Pick<Post, 'id'> & Partial<Post>>({
      query: ({ id, ...patch }) => ({
        url: `post/${id}`,
        method: 'PATCH',
        body: patch,
      }),
      async onQueryStarted({ id, ...patch }, { dispatch, queryFulfilled }) {
        const patchResult = dispatch(
          api.util.updateQueryData('getPost', id, (draft) => {
            Object.assign(draft, patch)
          }),
        )
        try {
          await queryFulfilled
        } catch {
          patchResult.undo()

          /**
           * Alternatively, on failure you can invalidate the corresponding cache tags
           * to trigger a re-fetch:
           * dispatch(api.util.invalidateTags(['Post']))
           */
        }
      },
    }),
  }),
})

You have to:

  • Implement onQueryStarted
  • Do const patchResult = dispatch(api.util.updateQueryData(endpointName, arg, updateCb)
  • await queryFulfilled
  • catch that and call patchResult.undo()

We could add a new option and a new lifecycle API method to simplify this.

Conceptual idea:

type OptimisticUpdateDefinition {
  endpointName: EndpointName;
  arg: Arg;
  update: (draft) => T
}

// then in a mutation endpoint:
applyOptimistic: (mutationArg: Arg, maybeSomeLifecycleApi: LifecycleApi) => (OptimisticUpdateDefinition | PatchCollection)[]

That way, the simple approach is to just list some endpoints you want updated, or you can import and reference another API slice to handle more complex cases where this API slice doesn't know about the endpoint:

applyOptimistic: (id) => [
  // simple case
  {endpointName: "todos", arg: id, update: (todos) => todos.push({id})},
  // more complex
  anotherApi.util.updateQueryData("otherTodos", id, (todos) => todos.push({id})
]

Internally, that would then convert the "simple" cases to PatchCollections as well, and then we could dispatch those, calling patchResult.undo() if needed. We could even consider batching all of them into a single dispatch, and still revert the patches individually if needed.

We could also add a lifecycleApi.applyOptimistic() helper that does the try/catch wrapping.

We could also also do the same thing for pessimistic updates.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions