Skip to content

Parallel Queries & Mutations

Import

import { queryById, mutationById } from '@ng-query/ngrx-signals-rxjs';

import { rxQueryById, rxMutationById } from '@ng-query/ngrx-signals-rxjs';

Overview

withQueryById and withMutationById are specialized versions of withQuery and withMutation designed for handling collections of resources identified by a unique key (such as an ID). They work with queryById/rxQueryById and mutationById/rxMutationById respectively.

Common use cases

Pagination

  • queryById can be used to cache multiples pages
  • mutationById can be used for parallel granular mutation (target a specific item of a page)

Cache multiples page data

  • by using queryById instead of query enable to cache all the data made by the same query (eg: A user details page that use the userId from the url to query the user. When using queryById it will saved all the visited user data in cache (in-memory by default))
  • In this scenario you should use mutationById to target a specific user query and enable parallel mutation

Specific Options queryById / rxQueryById / mutationById / rxMutationById

  • identifier: (mandatory)

    • Property to specify how each resource is grouped (e.g., by id).
    • This enables parallel management of multiple resources that are stored using a Map key/value, where the key is generated by the identifier
  • equalParams:

    • Only required when the params are an object.

    • Under the hood, a resource is generated for each new identifier generated when the params source change.

    • If the params source change, and their is an existing resource with the same identifier, it will be re-used.

    • In this case, when the source is an object, an existing resource can be retrieved by the matching his record key with identifier function, but as the reference change it will trigger the loading of the resource again.

    • To avoid this, you can use this option to tell how to compare the incoming params with the existing params of the resource.

      • 'useIdentifier': will use the identifier function to compare the previous params and the incoming params. This very useful when using pagination.
      • 'default' (default value): will use a strict equality check (===) between the previous params and the incoming params.
      • (a: Params, b: Params, identifierFn: (params: Params) => GroupIdentifier) => boolean: you can provide your own comparison function to compare the previous params and the incoming params. This is useful when you want to compare specific fields of the params.
    • Note: if your params is a primitive (string, number, boolean, etc.), you don't need to use this option since the strict equality check will work as expected.

    • For queries the default value is 'useIdentifier'

    • For mutations the default value is 'default'

  • params$:

    • Only available for queries and resources utilities.
    • Accept an observable as the params source
    • TODO backlog: can generate all the page/mutations as the methods
  • method:

    • Only available for mutation utilities
    • Will be exposed to the store to generate a mutation store.mutateXXXX(...)
    • TODO backlog: can generate all the page/mutations

Resource Options

  • For non rx utilities all the resource options, from the official doc
  • For rx utilities all the resource options, from the official doc

QueryById & MutationById Effects

When reacting to a mutationById, or trigger an imperative effect to a queryById, the filter option is mandatory:

  • filter:
    • When reacting to a queryById or mutationById, you should use a filter function to determine which resource(s) should be affected by an effect (optimistic update, patch, reload, etc).
    • The effect is only applied if filter returns true for the given identifier.

DANGER

queryById, rxQueryById mutationById and rxMutationById relies on signal source, only the last value emitted in very short period of time is considered. (A possible evolution is creating a withRxMutationById associated with rxMutationByIdthat relies on observables, the same for queries). For more info on this limitation

Usage Example

Store Setup

typescript
const Store = signalStore(
  withState({ usersFetched: [] as User[], selectedUserId: undefined as string | undefined }),
  withMutationById('user', () =>
    mutationById({
      method: (user: User) => user,
      loader: ({ params: user }) => updateUser(user),
      identifier: ({ id }) => id,
    })
  ),
  withQueryById(
    'user',
    (store) =>
      queryById({
        params: store.selectedUserId,
        loader: ({ params }) => fetchUser(params),
        identifier: (params) => params,
      }),
    () => ({
      on: {
        userMutationById: {
          optimisticPatch: {
            name: ({ mutationParams }) => mutationParams.name,
          },
          filter: ({ mutationParams, queryIdentifier }) => mutationParams.id === queryIdentifier,
        },
      },
    })
  )
);

You can access a specific resource by ID in your component:

typescript
// For queryById
const user5 = store.userQueryById()['5'];
if (user5?.hasValue()) {
  // Use user5 data
}

// for the mutationById
store.userMutationById()['5']?.hasValue();

// To mutate a specific user
store.mutateUser({ id: '5', name: 'New Name', email: 'new@example.com' });