Skip to content

Reacting to mutations

insertReactOnMutation declares the link between a write and the reads it affects: patch the query optimistically, reload it, or both — without calling refetch() from the mutation's call site.

Use it when a mutation makes some query stale. Not when the two are unrelated — a reaction that fires on every write is just a hidden coupling.

typescript
import { insertReactOnMutation } from '@craft-ng/core';

The common case

typescript
const updateUser = yield* mutation('updateUser', {
  method: (user: User) => user,
  loader: function* ({ params: user }) {
    return yield* CraftHttpClient.patch(({ response }) => ({
      url: `/api/users/${user.id}`,
      body: user,
      success: response<User>(),
    }));
  },
});

const queryRef = yield* query(
  'queryRef',
  {
    params: () => '5',
    loader: async ({ params }) => ({ id: params, name: 'John' }),
  },
  insertReactOnMutation(updateUser, {
    patch: {
      name: ({ mutationParams: { name } }) => name,
    },
  }),
);

Three levers, combinable:

OptionEffect
patchApply a field-by-field change once the mutation resolves
optimisticPatchApply it immediately, before the server answers
optimisticUpdateSame, but you compute the whole new value
reloadRe-run the loader — onMutationSuccess / onMutationException / onMutationResolved
filterOnly react when this predicate passes

The usual pairing is an optimistic change plus reload: { onMutationException: true } — show the result instantly, and go get the truth back if the write failed.

Targeting the right parallel query

With identifier, several query instances coexist. Use filter so the reaction only touches the one the mutation concerns:

typescript
const queryRef = yield* query(
  'queryRef',
  {
    params: userId,
    identifier: (userId) => userId,
    loader: function* ({ params }) {
      return yield* CraftHttpClient.get(({ response }) => ({
        url: `/api/users/${params}`,
        success: response<User>(),
      }));
    },
  },
  insertReactOnMutation(updateUser, {
    filter: ({ queryIdentifier, mutationParams }) =>
      mutationParams.id === queryIdentifier,
    patch: {
      name: ({ mutationParams: { name } }) => name,
    },
  }),
);

Several reactions on one query

A query accepts a single insertion, so compose them with insertQueryPipe to keep this composition readable:

typescript
import {
  insertQueryPipe,
  insertReactOnMutation,
  insertStoragePersister,
} from '@craft-ng/core';

const { users } = query(
  'users',
  {
    params: pagination,
    identifier: (params) => `${params.page}-${params.pageSize}`,
    loader: function* ({ params }) {
      return yield* ApiService.getDataList(params);
    },
  },
  insertQueryPipe(
      insertStoragePersister({ storeName: 'app', key: 'users' }),
      insertReactOnMutation(deleteUser, {
        filter: ({ mutationIdentifier, queryResource }) =>
          !!queryResource.value()?.some((u) => u.id === mutationIdentifier),
        optimisticUpdate: ({ queryResource, mutationIdentifier }) =>
          removeOne({
            entities: queryResource.value(),
            id: mutationIdentifier,
          }),
        reload: { onMutationException: true },
      }),
      insertReactOnMutation(deleteUser, {
        // reload the current page when it becomes empty
        filter: ({ queryResource }) => queryResource.value()?.length === 0,
        reload: { onMutationResolved: true },
      }),
      insertReactOnMutation(bulkDelete, {
        filter: ({ queryResource }) =>
          (queryResource.value()?.length ?? 0) > 0,
        optimisticUpdate: ({ queryResource, mutationParams }) =>
          removeMany({ entities: queryResource.value(), ids: mutationParams }),
      }),
    ),
);

Pitfalls

Optimistic without a fallback. optimisticPatch / optimisticUpdate show a change that has not happened yet. Pair them with reload: { onMutationException: true } so a failed write is corrected rather than silently left on screen.

Forgetting filter on parallel queries. Without it, a mutation on one entity patches every cached instance.

queryResource.value() returns undefined when the query is in exception; handle that case inside a filter.

See Also