DocsBlog
  • 1.3.0

  • light

    dark

    system

    Switch mode
  • Cerberus

    Acheron

    Elysium

Release - April 30, 2026

Cerberus v1.3 Release

CBAuthor's profile picture

Casey Baggz

Cerberus Admin

This release massively improves Signals performance along with minor React updates.

Overview

Here is a brief overview of what's new:

  • React Features - Button & Toast Notification updates
  • Signals Features - Signals become the Tanstack Query killer

React Features

The React updates contain some new features and stability fixes for the Button along with a breaking change for the toaster options regarding subtle notifications.

Button

We've added some new sizes to the Button recipe along with making the styling overall more stable when used in complex flex layouts.

New Button size range: xs up to 2xl
Default size: lg (this is visually the old medium)


Fix: Button no longer allows a flex parent to shrink its width. Likewise, buttons now have built in SVG size management when using non-strict width SVGs.

Checkout the Button documentation for more details.

Toast Notifications

Due to changes in the toaster engine there is a minor breaking change for subtle notifications (default notifications are un-affected).

Migration PathL:

  1. Subtle variants should use the default type name without the -subtle prefix.
  2. Subtle variants should now include usage: 'subtle' within the ToastOptions object.

Additionally, all deprecated APIs have been removed. These warnings have been around for almost a year - it's time.

While this update is not ideal, it does open the door to new Toast-related opportunities in the future.

View the Notification documentation for more details.

Other notable changes

Here are some high-level improvements made:

  1. Tooltip now accepts a portal prop to determine how to render content
  2. Data Grid has tooltip render issues from #1 - that's fixed
  3. useStatefulCollection now filters by label in addition to value
  4. New conditions: button, icon, iconButton, and menuTrigger

Signals Features

As of v1.3.0 - Signals are almost as performant as SolidJS and querys outperform Tanstack Query on multiple levels!

In this next phase of Signals, we have improved the scheduling algorithm along with the improving the query caching and query API. This has created a massive increaese in query performance which we will see later. 🚀

General Doc Updates

Across the board, we have updated a lot of doc pages and demos to provide more information around Signals: what they are, why they are important, and why you should be using them over React state/existing fetching designs.

New Primitives

Primitives are the building blocks to Cerby Signals. They can be called in any scope and used however you want. There are no boundaries with primitives.

Let's checkout the new APIs in this release.

Cleanup Helper

The onCleanup primitive is an essential memory-management tool within the Cerberus Signals reactivity system. It allows you to register a callback function that will execute right before a reactive context (like createEffect or createComputed) re-evaluates, or when that context is permanently destroyed.

In this example, we ensure a timer is cleared when it's parent effect is destroyed.


View the onCleanup docs

Untracking Signals

The untrack primitive allows you to read the current value of a signal inside a reactive context (like createEffect, createComputed, or a React render phase) without subscribing to that signal.

This prevents unwanted re-evaluations, React infinite loops, and over-subscribing to state changes.

In this example, we update a signal and show the latest result (tracked) and what the untracked version is.


View the untrack docs

Query Overhaul

Along with the signal engine, we have also improved the query cache and event algorithms which have allowed us to not only improve performance but expand the APIs.

Query Factory

When you call createQuery you are essentially creating a "Query Factory" to be used later to perform the fetch in however way you may want.

Creating a query now requires two things:

  1. A Promise to call when activated
  2. A unique key which is used to cache the result

Depending on how you want to use the factory depends on how you execute the query. You can use it in any shape, but for the sake of this article, we will discuss component usage only.

Client Component Pattern

Nothing changes here. Just pass the factory in the useQuery hook and have the data ready before the return hits.

const data = useQuery(myQuery(props.id))
Server Component Pattern

In an SSR environment, you execute the factory's raw fetcher directly, bypassing the reactive cache. Then, you pass that data down to your Client Components to "hydrate" or seed the Cerberus cache, ensuring the client doesn't double-fetch on mount.

This is the standard SSR pattern for React.

1. Server Component (Fetching)

Expose the raw, stateless fetcher function from your factory. You simply await it like a standard asynchronous function.

2. Client Component

On the client side, use the initialData property. If the Cerberus cache is empty, it will instantly seed the cache with the server's data, skipping the <Suspense> boundary entirely.


Streaming Reponses (Async Generators)

Cerberus queries also support streaming data via Async Generators. This is powerful if you are using an LLM API or your local API supports data streaming.


View the createQuery docs

Mutation Factory

We have added more options to the createMutation primitive which now includes optimistic updates. This means that you can have "real-time" UI while fetches automagically fetch in the background and update the query state when resolved.

In this example, we provide optimistic updates via the onMutate/setQueryData APIs which instantly update the UI while the query is run in the background.

If your optimistic update is incorrect, the UI will naturally resolve itself when the query completes.


View the createMutation docs

New Hooks

Unique store instances

When you want to use a unqiue instance of a global Store but do not need a Context API - use the useStore hook.


View the useStore docs

Benchmarks

Now let's talk about the good stuff - performance improvements with hard data.

Overall improvements

Cerby Signals were already performing at 0(1) but that is never good enough. Here is our own before/after with our engine upgrades:

Signal Benchmarks

BenchmarkOriginal (v1.2.1)Current (v1.3.0)Result
Deep Dependency318.17 µs226.83 µs~28% Faster
Wide Fan-Out2.26 ms3.17 ms~40% Slower
Diamond Problem323.05 µs458.60 µs~40% Slower

While this seems like a loss - it is still lightning fast compared to the upgraded ownership and versioning we have built into subscriptions to be more reliable.

The Reality of Reactivity

This is completely normal. Frameworks like SolidJS and Vue go through this exact pendulum swing. We traded raw, bare-metal speed for enterprise-grade memory safety (cleaning up event listeners and aborting network requests).

Even with the "regression," processing 10,000 effects in 3 milliseconds is still blazing fast and easily clears the 16ms frame budget for 60FPS UI updates.

Our next planned update should improve these metrics even further.

Now let's look at the real gains from this side effect: querys.

Query Benchmarks

BenchmarkOriginal (v1.2.1)Current (v1.3.0)Delta
Cache Retrieval (10k)8.88 ms1.68 ms~5.2x Faster
Retrieval Memory20.61 MB20.72 KB~99.9% Less Memory
Invalidation Sweep868.52 ns151.80 ns~5.7x Faster
Conclusion

While our internal onCleanup addition added a tiny microsecond tax to the raw scheduler engine, the query data layer you built on top of it is functioning at absolute peak efficiency. We traded ~80 nanoseconds in key hashing for a massive 500% speed increase in data retrieval and a 99.9% reduction in memory allocation.

Now let's put the rubber to the road and go head to head with Tanstack Query.

Cerberus Signals vs. TanStack Query

We ran a head-to-head performance benchmark comparing the Cerberus Signals data layer against the industry standard, TanStack Query Core. The tests were run in Bun (v1.2.16) on an Apple M3 Pro, measuring both raw execution speed and the memory footprint (p75/p99) of the Garbage Collector.

By leveraging an O(1) signal-based memory map instead of heavy class instantiations, Cerberus completely eliminates massive amounts of memory overhead while doubling the speed of standard operations.

Benchmark ScenarioMetricTanStack Query CoreCerberus SignalsThe Cerberus Advantage
Cache Retrieval(10,000 concurrent reads)Speed3.02 ms1.53 ms2x Faster
Memory59.62 KB639.72 bytes98.9% Less Memory
Cache Invalidation (Stale data sweep)Speed472.77 ns146.41 ns3.2x Faster
Memory12.29 bytes3.39 bytes72.4% Less Memory

Note: Memory measurements represent the p75/p99 allocations recorded during the benchmark loops.

The Takeaway: When scaling to thousands of reactive components, memory allocation becomes the primary bottleneck for UI thread frame rates. Cerberus Signals delivers enterprise-grade concurrency features (like Race-Condition Locks and React 19 Hydration) while consuming fractional kilobytes of memory compared to the megabytes allocated by traditional context-based providers.

Thanks!

This is another great release introducing some beneficial tools and features.

A special thanks to everyone who has helped validate the APIs, docs, and submitted features or bugfixes for this release.

There is no "I" in Cerber-"US"

Upgrading

To upgrade to this release, you can install the latest version of Cerberus React:

Terminal
Copy
npm run up:cerberus
Terminal
Copy
pnpm run up:cerberus
Terminal
Copy
deno run npm:up:cerberus
Terminal
Copy
bun run up:cerberus

Count: 0

Untracked: 0

Render Count: 1

Copy
'use client'

import { HStack, Stack } from '@/styled-system/jsx'
import { Button, Text } from '@cerberus/react'
import { ReactiveText, untrack, useStore } from '@cerberus/signals'
import { useEffect } from 'react'
import { createRenderStore } from '../render-store'

export function BasicDemo() {
  const store = useStore(createRenderStore)

  const increment = () => {
    store.setCount((prev) => prev + 1)
  }

  useEffect(() => {
    store.startCounter()
    return () => store.onUnmount()
  }, [store])

  store.trackRenders()

  return (
    <HStack justify="space-between" w="3/4">
      <Button onClick={increment}>Increment</Button>

      <Stack>
        <Text>
          Count: <ReactiveText data={store.count} />
        </Text>
        <Text>Untracked: {untrack(store.count)}</Text>
        <Text>
          Render Count: <ReactiveText data={store.renderCount} />
        </Text>
      </Stack>
    </HStack>
  )
}
Copy
idle
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
  },
}

Count: 0

Render Count: 1

Copy
'use client'

import { HStack, Stack } from '@/styled-system/jsx'
import { Button, Text } from '@cerberus/react'
import { ReactiveText, useStore } from '@cerberus/signals'
import { useEffect } from 'react'
import { createRenderStore } from '../render-store'

export function UseStoreDemo() {
  const store = useStore(createRenderStore)

  const increment = () => {
    store.setCount((prev) => prev + 1)
  }

  useEffect(() => {
    return () => store.onUnmount()
  }, [store])

  store.trackRenders()

  return (
    <HStack justify="space-between" w="3/4">
      <Button onClick={increment}>Increment</Button>

      <Stack>
        <Text>
          Count: <ReactiveText data={store.count} />
        </Text>
        <Text>
          Render Count: <ReactiveText data={store.renderCount} />
        </Text>
      </Stack>
    </HStack>
  )
}
Copy
import { Button, ButtonProps, For } from '@cerberus/react'
import { HStack } from 'styled-system/jsx'
import recipesSpec from 'styled-system/specs/recipes.json'

const sizes = (recipesSpec.data.find((r) => r.name === 'button')?.variants.size ??
  []) as ButtonProps['size'][]

export function SizeDemo() {
  return (
    <HStack>
      <For each={sizes}>
        {(size) => (
          <Button key={size} size={size}>
            {size}
          </Button>
        )}
      </For>
    </HStack>
  )
}
'use client'

import { useQuery } from '@cerberus/signals'
import { queryUser } from './queries'
import { type User } from './db'

interface Props {
  id: string
  initialData: User
}

export function UserProfile(props: Props) {
  // 1. Cerberus sees `initialData`, instantly seeds the $O(1) Map,
  // and mounts the component with zero loading spinners or network waterfalls.
  const user = useQuery(queryUser(props.id), { initialData: props.initialData })

  if (!user) return null

  return <h1>{user.name}</h1>
}
// app/users/[id]/page.tsx (Server Component)

import { UserProfile } from './client.demo'
import { queryUser } from './queries'

interface Props {
  params: Promise<{ id: string }>
}

export default async function UserPage({ params }: Props) {
  const { id } = await params
  // Bypass the reactive cache and execute the raw fetcher directly.
  // This is completely memory-safe for Node.js environments.
  const initialUserData = await queryUser.fetcher(id)

  if (!initialUserData) return null

  return (
    <main>
      {/* 2. Pass the fetched data to the Client Component */}
      <UserProfile id={id} initialData={initialUserData} />
    </main>
  )
}
import { createEffect, onCleanup } from '@cerberus/signals'

createEffect(() => {
  const timer = setInterval(() => console.log('Tick'), 1000)
  onCleanup(() => clearInterval(timer))
})
'use client'

import { Stack } from '@/styled-system/jsx'
import { Button, Field, Group, Input, Show, Text } from '@cerberus/react'
import { createQuery, useQuery, useSignal } from '@cerberus/signals'
import { ChangeEvent, Suspense } from 'react'

// --- 1. Fake Streaming LLM API ---
// Simulates an AI sending text chunks over a network
async function* fakeLLMStream(prompt: string) {
  const responseText = `You asked: "${prompt}".\n\nThis is a simulated streaming response from the Cerberus Signals engine. Notice how the text appears chunk by chunk. Because the query engine natively understands Async Generators, it bypasses React Suspense boundaries mid-stream, achieving an instant, seamless real-time UX without complex socket bindings.`

  const words = responseText.split(' ')

  for (let i = 0; i < words.length; i++) {
    // Simulate 50ms network delay per word chunk
    await new Promise((res) => setTimeout(res, 50))
    yield words[i] + ' '
  }
}

// --- 2. Define Streaming Query Factory ---
// The fetcher uses `async function*` and yields the accumulated state
const getChatStream = createQuery(async function* (prompt: string) {
  let fullText = ''

  // Listen to the stream and accumulate chunks
  for await (const chunk of fakeLLMStream(prompt)) {
    fullText += chunk
    // The moment this yields, Cerberus sets the cache status to 'streaming'
    // and passes the partial data directly to the UI
    yield fullText
  }
}, 'queryChatStream')

// --- UI ---

function ChatWindow(props: { prompt: string }) {
  const chatResponse = useQuery(getChatStream(props.prompt))
  return (
    <Stack gap="md" p="md" bg="surface.container" rounded="md">
      <Text textStyle="body-md">{chatResponse}</Text>
    </Stack>
  )
}

export function StreamingDemo() {
  // we use hooks to force re-renders of the form content
  const [input, setInput] = useSignal('How does Cerberus handle streams?')
  const [submittedPrompt, setSubmittedPrompt] = useSignal('')

  return (
    <Stack gap="lg" w="3/4">
      <Show when={submittedPrompt}>
        {() => (
          <Suspense
            fallback={
              <Text as="small" textStyle="body-xs">
                Thinking...
              </Text>
            }
          >
            <ChatWindow prompt={submittedPrompt} />
          </Suspense>
        )}
      </Show>

      <Group layout="attached" w="full">
        <Field required>
          <Input
            onChange={(e: ChangeEvent<HTMLInputElement>) => setInput(e.target.value)}
            value={input}
            borderTopRightRadius="none"
            borderBottomRightRadius="none"
            h="2.75rem"
          />
        </Field>
        <Button onClick={() => setSubmittedPrompt(input)}>Send Prompt</Button>
      </Group>
    </Stack>
  )
}

User d817d2ab-7258-4ef8-80fd-9045bd7994fe