DocsBlog
  • 1.3.0

  • light

    dark

    system

    Switch mode
  • Cerberus

    Acheron

    Elysium

Get Started
Components
Data Grid
Signals
Styling
Theming

Get Started

OverviewReactivityData FetchingStores

Primitives

createSignalcreateQueryNewcreateMutationNewcreateComputedcreateEffectcreateStoreContextbatchonCleanupNewuntrackNew

Hooks

useQueryNewuseMutationuseReaduseSignaluseStoreNew

Components

ReactiveText

On this page

Loading...

Loading...

Loading...

Loading...

Creating Mutations

Learn how to update queries in React.

  • source
import { createMutation, useMutation } from '@cerberus/signals'

Usage

createMutation allows you create a Mutation Factory that works alongside Cerberus Queries. Mutations provide optimistic updates and invalidation options to keep your UI seamlessly "real-time" by fetching in the background on your behalf.

Mutations are used when you want to make updates to data that is associated with a query.

Likewise, if you have a query that will never have update-related actions associated with it - you don't need mutations.

Creating Mutations

Creating a mutation requires two things:

  1. A Promise to call when activated
  2. Mutation options to utilize when mutate is called

Ultimately, when you create a mutation you are really just creating a factory that can be called where ever and whenever you are ready.

Using Mutations

Depending on your goals will determine how to you the mutation. The following sections outline the different use cases whether you are optimistically updating or not.

Mutations can live at any level. All that matters is that the factory has access to the query.key to reference.

There are no restrictions to Cerberus Signal Primitives which means you can use them at any scope.

Pattern 1: Optimistic Updates

Optimistic updates allow you to update the state of your query data which which will render in the UI immediately while calling the Promise-based mutation in the background.

This is done by using the setQueryData helper function.

Utilizing this helper will allow your application to appearingly have "real-time" functionality.

Once the mutation is settled, the return value will automagically overwrite the optimistic value. If these are the same - there will be no visual difference in the UI. If there is a descrepency, the UI will automagically be corrected.

To use optimitic updates, add the onMutate method to the options Object of your second argument and call the setQueryData helper to update the state.

const updateUser = createMutation((payload: User) => api.updateUser(payload), {
    // Optimistically update the UI instantly
    onMutate: (vars) => {
      setQueryData<User>(getUser.key(vars.id), (prev) => {
        if (!prev) return { id: vars.id, name: vars.name }
        return { ...prev, name: vars.name }
      })
    },
  })

The setQueryData helper accepts a query.key with the same arguments that the original query expects. This is done by using the key property from the original query.

Pattern 2: Non-optimistically

If you are interested in a legacy approach that matches traditional application updates via visual loading states - simply omit the onMutate option (and thus not using setQueryData).

onMutate does not create the optimistic update - setQueryData does. So, if you needed the onMutate lifecycle method you can still use it.

Within Components

To use mutations within a component, pass the factory to the useMutation hook. This will return an Object that contains the mutate method and any of the other return properties.

const { mutate, ...state } = useMutation(myMutation)

The Mutation Lifecycle

When you trigger a mutation that includes onMutate and invalidate handlers, Cerberus orchestrates a complex, highly-optimized sequence of events under the hood.

Understanding this lifecycle is key to mastering Optimistic UI. Here is the exact chronology of what happens the millisecond you call mutate():

1. The Trigger

The user clicks a button and your component calls mutate(payload).

2. The Optimistic Update (onMutate)

Before the network request even begins, Cerberus synchronously executes your onMutate callback.

  • You use setQueryData to inject the expected result directly into the cache.
  • The Engine Lock: Cerberus instantly increments an internal version integer on that specific cache node.
  • The UI Update: The UI instantly snaps to the new state. Because this uses direct signal memory pointers, it executes in ~130 nanoseconds with zero main-thread blocking.

3. The Network Request

The asynchronous mutation function you defined in the factory (e.g., api.updateUser) is fired to the server. Your component can track this loading state using the isolated pending status.

4. Background SWR (invalidate)

Once the server responds successfully, your invalidate callback fires.

  • It flags the target query cache as "stale."
  • Because the UI already has data (your optimistic update), Cerberus intentionally bypasses React Suspense boundaries.
  • A background Stale-While-Revalidate (SWR) fetch is quietly triggered to verify the data against the database. The user experiences absolutely zero UI flickering.

5. Resolution & The Version Lock

When the background SWR fetch finally resolves, the engine performs one final, critical check before saving the data: The Version Lock.

  • It compares the cache's current version to the version it was when the fetch started.
  • If they match: The background data perfectly overwrites the cache, confirming the database is in sync with the UI.
  • If they differ: This means the user rapidly clicked mutate() again while the background fetch was running. Cerberus realizes this fetch is now stale, intercepts the data, and silently aborts the update. This guarantees your users will never experience a "Flash of Unstyled Content" (FOUC) or see their UI snap backward during rapid interactions.

Timeline Visualization

TimeActionUI StateBackground Engine
0msUser calls mutate()Instantly newversion: 1, Cache updated
5msNetwork request startsInstantly newMutation pending
800msNetwork request succeedsInstantly newinvalidation triggers SWR
805msBackground query startsInstantly newfetching database state...
1800msBackground query finishesStays consistentVersion check -> Cache synced

API

createMutation accepts the following options:

ParamsRequiredDescription
mutatortrueThe Promise-based function used when the mutate is called.
optionsfalseAn Object of conditional options for defined mutations.

Mutation Options

The options Objecct contains the following optional properties:

PropertyTypeDescription
onMutate(variables: V) => voidA callback to run when the mutation is triggered.
onSuccess(data: T, variables: V) => voidA callback to run when the mutation is successful.
onError(error: unknown, variables: V) => voidA callback to run when the mutation has errored.
invalidate'all' | ((data: T, variables: V) => string[])A list of query.key to invalidate once the mutation has completed.

Return

useMutation returns a MutationReturn Object that contains the following properties:

IndexTypeDescription
mutate(variables: V) => Promise<T>A function used to trigger the mutation.
status'idle' | 'pending' | 'success' | 'error'The Promise-based status of the mutation.
errorunknown | undefinedThe error thrown when something bad happens.
dataT | undefinedThe data returned from the mutation.

setQueryData

setQueryData doesn't return anything and accepts following properties:

IndexTypeDescription
hashKeystringA unique key provide from calling the query.key method with the expected arguments passed in provided from the onMutate parameters.
updater(prev: T | undefined) => TA setter function that returns the query data state if existing and expects the new state returned.
Copy
'use client'

import { Box, HStack, Stack } from '@/styled-system/jsx'
import { Button, Tag, Text } from '@cerberus/react'
import {
  createMutation,
  createQuery,
  setQueryData,
  useMutation,
  useQuery,
} from '@cerberus/signals'
import { Suspense } from 'react'

// 1. Define Query Factory
const getUser = createQuery(async (id: string) => {
  return await api.getUser(id)
}, 'queryGetUser')

// 2. Define Mutation Factory
const updateUser = createMutation((payload: User) => api.updateUser(payload), {
  // Optimistically update the UI instantly
  onMutate: (vars) => {
    setQueryData<User>(getUser.key(vars.id), (prev) => {
      if (!prev) return { id: vars.id, name: vars.name }
      return { ...prev, name: vars.name }
    })
  },
  // Declarative cache invalidation using factory keys
  invalidate: (_data, vars) => [getUser.key(vars.id)],
})

// 3. Consume in Components seamlessly
function UserProfile(props: { id: User['id'] }) {
  const user = useQuery(getUser(props.id))
  return <Text>{user.name}</Text>
}

export function MutationDemo() {
  // Pretend this is from the URL or via props.id
  const id = crypto.randomUUID()

  const { mutate, status } = useMutation(updateUser)

  function handleUpdate() {
    mutate({ id, name: `User ${crypto.randomUUID()}` })
  }

  return (
    <HStack gap="lg" w="3/4">
      <Button onClick={handleUpdate}>Update Name</Button>

      <Stack>
        <Suspense fallback={<Box aria-busy h="44px" rounded="sm" w="366px" />}>
          <UserProfile id={id} />
        </Suspense>
        <Tag w="fit-content">{status}</Tag>
      </Stack>
    </HStack>
  )
}

// API

type User = { id: string; name: string }

const fakeDB = new Map<string, User>()
const delay = (ms: number) => new Promise((res) => setTimeout(res, ms))

const api = {
  getUser: async (id: User['id']) => {
    await delay(500)
    if (!fakeDB.has(id)) {
      fakeDB.set(id, { id, name: `User ${id}` })
    }
    return fakeDB.get(id)!
  },
  updateUser: async (payload: User) => {
    await delay(500)
    fakeDB.set(payload.id, { id: payload.id, name: payload.name })
    return payload
  },
}
Copy
'use client'

import { Box, HStack, Stack } from '@/styled-system/jsx'
import { Button, Tag, Text } from '@cerberus/react'
import {
  createMutation,
  createQuery,
  setQueryData,
  useMutation,
  useQuery,
} from '@cerberus/signals'
import { Suspense } from 'react'

// 1. Define Query Factory
const getUser = createQuery(async (id: string) => {
  return await api.getUser(id)
}, 'queryGetUser')

// 2. Define Mutation Factory
const updateUser = createMutation((payload: User) => api.updateUser(payload), {
  // Optimistically update the UI instantly
  onMutate: (vars) => {
    setQueryData<User>(getUser.key(vars.id), (prev) => {
      if (!prev) return { id: vars.id, name: vars.name }
      return { ...prev, name: vars.name }
    })
  },
  // Declarative cache invalidation using factory keys
  invalidate: (_data, vars) => [getUser.key(vars.id)],
})

// 3. Consume in Components seamlessly
function UserProfile(props: { id: User['id'] }) {
  const user = useQuery(getUser(props.id))
  return <Text>{user.name}</Text>
}

export function MutationDemo() {
  // Pretend this is from the URL or via props.id
  const id = crypto.randomUUID()

  const { mutate, status } = useMutation(updateUser)

  function handleUpdate() {
    mutate({ id, name: `User ${crypto.randomUUID()}` })
  }

  return (
    <HStack gap="lg" w="3/4">
      <Button onClick={handleUpdate}>Update Name</Button>

      <Stack>
        <Suspense fallback={<Box aria-busy h="44px" rounded="sm" w="366px" />}>
          <UserProfile id={id} />
        </Suspense>
        <Tag w="fit-content">{status}</Tag>
      </Stack>
    </HStack>
  )
}

// API

type User = { id: string; name: string }

const fakeDB = new Map<string, User>()
const delay = (ms: number) => new Promise((res) => setTimeout(res, ms))

const api = {
  getUser: async (id: User['id']) => {
    await delay(500)
    if (!fakeDB.has(id)) {
      fakeDB.set(id, { id, name: `User ${id}` })
    }
    return fakeDB.get(id)!
  },
  updateUser: async (payload: User) => {
    await delay(500)
    fakeDB.set(payload.id, { id: payload.id, name: payload.name })
    return payload
  },
}
Copy
'use client'

import { Box, HStack, Stack } from '@/styled-system/jsx'
import { Button, Tag, Text } from '@cerberus/react'
import {
  createMutation,
  createQuery,
  useMutation,
  useQuery,
} from '@cerberus/signals'
import { Suspense } from 'react'

// 1. Define Query Factory
const getUser = createQuery(async (id: string) => {
  return await api.getUser(id)
}, 'queryGetUser')

// 2. Define Mutation Factory
const updateUser = createMutation((payload: User) => api.updateUser(payload), {
  // Declarative cache invalidation using factory keys
  invalidate: (_data, vars) => [getUser.key(vars.id)],
})

// 3. Consume in Components seamlessly
function UserProfile(props: { id: User['id'] }) {
  const user = useQuery(getUser(props.id))
  return <Text>{user.name}</Text>
}

export function NoOptmisticDemo() {
  // Pretend this is from the URL or via props.id
  const id = crypto.randomUUID()

  const { mutate, status } = useMutation(updateUser)

  function handleUpdate() {
    mutate({ id, name: `User ${crypto.randomUUID()}` })
  }

  return (
    <HStack gap="lg" w="3/4">
      <Button pending={status === 'pending'} onClick={handleUpdate}>
        <Button.Icon />
        Update Name
      </Button>

      <Stack>
        <Suspense fallback={<Box aria-busy h="44px" rounded="sm" w="366px" />}>
          <UserProfile id={id} />
        </Suspense>
        <Tag w="fit-content">{status}</Tag>
      </Stack>
    </HStack>
  )
}

// API

type User = { id: string; name: string }

const fakeDB = new Map<string, User>()
const delay = (ms: number) => new Promise((res) => setTimeout(res, ms))

const api = {
  getUser: async (id: User['id']) => {
    await delay(500)
    if (!fakeDB.has(id)) {
      fakeDB.set(id, { id, name: `User ${id}` })
    }
    return fakeDB.get(id)!
  },
  updateUser: async (payload: User) => {
    await delay(500)
    fakeDB.set(payload.id, { id: payload.id, name: payload.name })
    return payload
  },
}
idle
idle
idle

On this page

  • Usage
  • Creating Mutations
  • Using Mutations
  • Pattern 1: Optimistic Updates
  • Pattern 2: Non-optimistically
  • Within Components
  • The Mutation Lifecycle
  • 1. The Trigger
  • 2. The Optimistic Update (`onMutate`)
  • 3. The Network Request
  • 4. Background SWR (`invalidate`)
  • 5. Resolution & The Version Lock
  • Timeline Visualization
  • API
  • Mutation Options
  • Return
  • `setQueryData`
  • Edit this page on Github

User b60013f2-981c-447d-971d-7ee4844e136f

User d69a9ff6-0c32-44ee-a30b-6e17bce903ae

User 74d97ee8-daea-4770-9e3d-d614826ee50c