DocsBlog
  • 1.1.2

  • light

    dark

    system

    Switch mode
  • Cerberus

    Acheron

    Elysium

Release - March 23, 2026

Cerberus v1.1 Release

CBAuthor's profile picture

Casey Baggz

Cerberus Admin

This release adds new features, packages, and improvements to our entire ecosystem.

Overview

Kicking v1.1 with a bang...or should I say...signal? 👀

Takes note: never write that joke again... 😂

Here is a brief overview of what's new:

  • React Features - new primitive APIs and general updates
  • Data Grid Features - Data Grid gets some new goodies
  • Signals - React signal-based management and data-fetching at 0(1) speed

React Features

Tabs

We have improved the Tabs API to support vertical orienation via the orientation and indicatorPosition props.

The indicatorPosition will decide where the active indicator (e.g., line) is positioned matching the start/end positioning of our other position-related APIs.


Checkout the Tabs documentation for more details.

Data Grid Features

Our goal is to keep the Data Grid API growing with each release. We have two important updates with v1.1:

Footer Slot

We've added support for a footer slot that will allow you to add custom content within the scope of the Grid Store via the footer prop.

This works just like the toolbar in regards to just drop in the component you want rendered in that slot (and optionally use the useDataGridContext to get juicy data).


Checkout the Data Grid footer documentation for more details.

Theming

Sometimes, you may have an intricate design that calls for the Data Grid but with some visual tweaks. We help streamline that effort with the new theme prop.

Overall, the Data Grid doesn't style that much. The true style of the Grid comes from the cell content rendered.

Thus, the ThemeOptions offered to pass into the theme prop are the only safe styles we define for the layout.


Checkout the Data Grid Theme documentation for more details.

Signals

In v1.1 we introduce potentially one of the most impactful tools to the React ecosystem since Tanstack: Cerberus Signals.

The Problem

Since migrating to hooks, React has become more than it was ever orignally designed to be as an API. This means that it has scaled past its architecture. Especially, now that it is saturated in the market, the team can no longer move in the direction they need to for the best result for its users.

Thus, the rise of libraries like SolidJS who introduced the "Signal" pattern as an external state manager - validating there was hope for the future of frontend development.

Current Solutions

There have been many external state libraries for React before Cerberus Signals. Rrom Redux to the closest signal-based solution Legend-State. However, all have fallen short in some area whether architecture or developer experience.

We believe that we have fixed and improved in all of those areas. Let's dive in.

Cerberus Signals is highly inspired by SolidJS in more than one way. This wouldn't exist without the Solid core team pushing boundaries and dreaming big.

State managent

Signal-based state managent is the foundation of Cerberus Signals. We intentionally avoid the use of Proxies which compliment how the React engine works. A positive side-effect of this is also providing a better developer experience for debugging.

If you have worked with Proxy-based state before, you know what we mean. 😉

With Cerberus signals, you can create global signals via createSignal or local (component-based) via the useSignal hook.


Read the Reactivity documentation

Stores

Sometimes you may want a global single-source of truth for a chunk of signals and actions to create a scalable reactive API. This is how we design the Data Grid context!

In Cerberus Signals, you can do this elegantly without any additional APIs.


Read the Stores documentation

Effects & Composition

Since Cerberus Signals are external to the React render engine, there is no longer a need for native APIs like useMemo or useEffect (unless you want to bring in the component lifecycle).

Thus, you can createComputed values from multiple signals and watch for changes with createEffect.

Since these APIs are not related to React, you can use them on any scope. Likewise, effects can be nested if you wanted to.

Did we mention no dependency arrays? You don't need them with Cerberus Signals.


  • Read the Computed documentation
  • Read the Effects documentation

Querys & Mutations

One of the best parts of SolidJS is the magical signal-based fetching. We provide a similar API via queries.

We use the term "fetching" but queries resolve any Promise. The only limitation to what a query is for your application is your own imagination.

Even more, when a query is referencing a signal - it will autofetch when the signal updates along with caching the result until it is invalidated.

There are two steps to creating a signal-based promise:

  1. Define a query via createQuery
  2. Use the query in a component via the useQuery hook.

If you reference a signal in a query you do not need mutations. Just simply manage the signal being referenced.

💡 Any Cerberus query-related API is compatible with React Suspense and Error Boundaries out of the box.


  • Read the query documentation
  • Read the mutation documentation

Benchmarks

Now for the most important part - the hard data.

None of this matters if this design doesn't scale. The goal for Cerberus Signals was to allow React developers to have as close to a SolidJS experience as possible in terms of performance and JSX rendering.

If we can't make React render at 0(1) it's a waste...

Today I'm proud to say the benchmarks on the APIs validate the effort and passion of our work has paid off.

Signal Benchmarks:

Benchmark results for the signal APIs

Let's break down the results:

  • The Diamond Problem (328 µs/iter): This is our crowning achievement. We just proved that iterating over a Set of 1,000 computeds, sorting them by depth, and executing them top-down takes 0.3 milliseconds.
  • Wide Fan-Out (2.10 ms/iter): A buttery-smooth 60 frames-per-second UI gives you a budget of 16.6 ms per frame. Our graph can notify and execute 10,000 individual effects in just 2.1 ms. That leaves over 14 milliseconds for React to do its actual DOM painting.
  • Deep Dependency Chain (318 µs/iter): A 1,000-level deep calculation resolved in a third of a millisecond. This proves our scheduler and isFlushing lock are incredibly efficient and prevent stack overflows.

Query Benchmarks:

Benchmark results for the signal APIs

These numbers are phenomenal:

    1. The hashArgs Victory (398 ns) Sorting object keys and running JSON.stringify is notoriously dangerous in JavaScript because it can block the main thread. Our implementation is executing in 398 nanoseconds.
    • The Reality: A microsecond is 1,000 nanoseconds. A millisecond is 1,000 microseconds. It takes a browser roughly 16,000 microseconds to paint a single frame. Our hashing algorithm is taking less than half of one microsecond. It is virtually invisible to the CPU.
    1. Cache Retrieval & Deduplication (9.08 ms for 10,000 calls) This is the most critical benchmark for our Data Grid. If a user renders 10,000 rows, and each row calls useQuery for a shared configuration or parent data state, React has to mount 10,000 hooks.
    • The Reality: Our library processes all 10,000 of those cache lookups and deduplicates them in 9 milliseconds. That averages out to 0.0009 milliseconds per query call. It proves that our createSignal wrapper will never be the bottleneck in a massive React application.

Installing Signals

If you are interested in adopting signals, you can install it today:

Terminal
Copy
npm install @cerberus/signals@npm:@cerberus-design/signals
Terminal
Copy
pnpm add @cerberus/signals@npm:@cerberus-design/signals
Terminal
Copy
deno add npm:@cerberus/signals@npm:@cerberus-design/signals
Terminal
Copy
bun add @cerberus/signals@npm:@cerberus-design/signals

Thanks!

This is another monumental release introducing a massive tool to the React community via Cerberus Signals.

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
Copy
import { Box } from '@/styled-system/jsx'
import { Tabs } from '@cerberus/react'

export function VerticalDemo() {
  return (
    <Box w="full">
      <Tabs.Root
        defaultValue="overview"
        orientation="vertical"
        indicatorPlacement="start"
      >
        <Tabs.List>
          <Tabs.Tab value="overview">Overview</Tabs.Tab>
          <Tabs.Tab value="features">Features</Tabs.Tab>
          <Tabs.Tab value="pricing">Pricing</Tabs.Tab>
        </Tabs.List>
        <Tabs.Panel value="overview">Overview content</Tabs.Panel>
        <Tabs.Panel value="features">Features content</Tabs.Panel>
        <Tabs.Panel value="pricing">Pricing content</Tabs.Panel>
      </Tabs.Root>
    </Box>
  )
}
Copy
'use client'

import { DecorativeBox } from '@/app/components/decorative-box'
import { Stack } from '@/styled-system/jsx'
import { DataGrid } from '@cerberus/data-grid'
import { columns } from '../quick-start/columns.demo'
import { createFakeQuery } from '../quick-start/data'

export function BasicDemo() {
  const data = createFakeQuery(10)

  return (
    <Stack direction="column" gap="md" h="25rem" mb="md" w="90%">
      <DataGrid
        columns={columns}
        data={data()}
        toolbar={<Toolbar />}
        footer={<Footer />}
      />
    </Stack>
  )
}

function Toolbar() {
  return <DecorativeBox h="50px">Toolbar</DecorativeBox>
}

function Footer() {
  return <DecorativeBox h="50px">Footer</DecorativeBox>
}
Copy
'use client'

import { DataGrid, ThemeOptions } from '@cerberus/data-grid'
import { For, Text } from '@cerberus/react'
import { Center, HStack, VStack } from 'styled-system/jsx'
import { createFakeQuery } from '../quick-start/data'
import { columnHelper } from '../quick-start/helper.demo'

const customTheme: ThemeOptions = {
  border: 'none',
  borderColor: 'transparent',
  rowBgColor: 'var(--cerberus-colors-page-surface-initial)',
  rowEvenBgColor: 'var(--cerberus-colors-page-surface-initial)',
  rowHoverBgColor: 'transparent',
  headCellBorderBottomColor: 'transparent',
}

const columns = Array.from({ length: 4 }, (_, i) =>
  columnHelper.display({
    id: `col-${i}`,
    header: () => (
      <Text
        as="strong"
        display="block"
        ms="lg"
        fontWeight="bold"
        textStyle="label-sm"
      >{`Column ${i + 1}`}</Text>
    ),
    cell: () => (
      <HStack bgColor="black" justify="center" h="full" me="sm" p="sm" w="full">
        <Center bgColor="red" p="sm" rounded="md" w="full">
          1.0
        </Center>
      </HStack>
    ),
  }),
)

export function BasicDemo() {
  // Normally this would be from useQuery or a server-side API call
  const data = createFakeQuery(10)

  return (
    <HStack h="20rem" w="3/4">
      <Legend />
      <DataGrid columns={columns} data={data()} rowSize="lg" theme={customTheme} />
    </HStack>
  )
}

function Legend() {
  return (
    <VStack gap="0" justify="center" h="full" pt="15px">
      <For each={columns}>
        {(column) => (
          <Center key={column.id} h="56px" w="full">
            <Text
              as="strong"
              display="block"
              fontWeight="bold"
              textStyle="label-sm"
              transform="rotate(-90deg)"
            >
              Kewl
            </Text>
          </Center>
        )}
      </For>
    </VStack>
  )
}
Copy
'use client'

import { Box, Stack } from '@/styled-system/jsx'
import { Button } from '@cerberus/react'
import { createQuery, createSignal, useQuery } from '@cerberus/signals'
import { Suspense } from 'react'

type User = {
  id: string
  name: string
}

function fetchUser(id: User['id']): Promise<User> {
  return new Promise<User>((resolve) => {
    setTimeout(() => {
      resolve({ id, name: `User ${id}` })
    }, 1000)
  })
}

const [currentUser, setCurrentUser] = createSignal<User['id']>(crypto.randomUUID())

// 1. Define the query
const query = createQuery(currentUser, fetchUser)

function UserInfo() {
  // 2. Trigger the query
  const data = useQuery(query)
  return <pre>{JSON.stringify(data, null, 2)}</pre>
}

export function BasicDemo() {
  return (
    <Stack direction="column" justify="space-between" w="3/4">
      <Suspense fallback={<Box aria-busy h="96px" rounded="sm" w="full" />}>
        <UserInfo />
      </Suspense>

      <Button onClick={() => setCurrentUser(crypto.randomUUID())}>Change User</Button>
    </Stack>
  )
}
Copy
'use client'

import { HStack } from '@/styled-system/jsx'
import { Button, Text } from '@cerberus/react'
import { createSignal, ReactiveText } from '@cerberus/signals'

function createDemoStore() {
  const [count, setCount] = createSignal<number>(0)
  const [renderCount, setRenderCount] = createSignal<number>(0)
  return { count, setCount, renderCount, setRenderCount }
}

export function BasicDemo() {
  const store = createDemoStore()
  const increment = () => store.setCount(store.count() + 1)

  store.setRenderCount(store.renderCount() + 1)

  return (
    <HStack justify="space-between" w="3/4">
      <HStack gap="md" w="full">
        <Button onClick={increment}>Increment</Button>
        <ReactiveText data={store.count} />
      </HStack>

      <HStack gap="md" w="full">
        <Text>Render Count:</Text>
        <ReactiveText data={store.renderCount} />
      </HStack>
    </HStack>
  )
}
0:0:0
Copy
'use client'

import { HStack } from '@/styled-system/jsx'
import { Button } from '@cerberus/react'
import {
  batch,
  createComputed,
  createEffect,
  createSignal,
  ReactiveText,
  type Accessor,
} from '@cerberus/signals'

type ClockStore = {
  time: Accessor<string>
  hours: Accessor<number>
  minutes: Accessor<number>
  seconds: Accessor<number>
  shortTime: Accessor<string>
  startClock: () => void
}

export function globalStoreDemo(): ClockStore {
  const [hours, setHours] = createSignal<number>(0)
  const [minutes, setMinutes] = createSignal<number>(0)
  const [seconds, setSeconds] = createSignal<number>(0)
  const [turnOn, setTurnOn] = createSignal<boolean>(false)

  const time = createComputed<string>(() => `${hours()}:${minutes()}:${seconds()}`)
  const shortTime = createComputed<string>(() => `${hours()}:${minutes()}`)

  function getTime() {
    const now = new Date()
    setHours(now.getHours())
    setMinutes(now.getMinutes())
    setSeconds(now.getSeconds())
  }

  createEffect(() => {
    if (turnOn()) {
      const interval = setInterval(() => getTime(), 1000)
      return () => clearInterval(interval)
    }
  })

  return {
    time,
    shortTime,
    hours,
    minutes,
    seconds,
    // actions
    startClock: () => {
      batch(() => {
        setTurnOn(true)
        getTime()
      })
    },
  }
}

export function GlobalDemo() {
  const store = globalStoreDemo()
  return (
    <HStack gap="md" w="3/4">
      <Button onClick={store.startClock}>Start</Button>
      <ReactiveText data={store.time} />
    </HStack>
  )
}
0:0:0
Copy
'use client'

import { HStack } from '@/styled-system/jsx'
import { Button } from '@cerberus/react'
import {
  batch,
  createComputed,
  createEffect,
  createSignal,
  ReactiveText,
  type Accessor,
} from '@cerberus/signals'

type ClockStore = {
  time: Accessor<string>
  hours: Accessor<number>
  minutes: Accessor<number>
  seconds: Accessor<number>
  shortTime: Accessor<string>
  startClock: () => void
}

export function globalStoreDemo(): ClockStore {
  const [hours, setHours] = createSignal<number>(0)
  const [minutes, setMinutes] = createSignal<number>(0)
  const [seconds, setSeconds] = createSignal<number>(0)
  const [turnOn, setTurnOn] = createSignal<boolean>(false)

  const time = createComputed<string>(() => `${hours()}:${minutes()}:${seconds()}`)
  const shortTime = createComputed<string>(() => `${hours()}:${minutes()}`)

  function getTime() {
    const now = new Date()
    setHours(now.getHours())
    setMinutes(now.getMinutes())
    setSeconds(now.getSeconds())
  }

  createEffect(() => {
    if (turnOn()) {
      const interval = setInterval(() => getTime(), 1000)
      return () => clearInterval(interval)
    }
  })

  return {
    time,
    shortTime,
    hours,
    minutes,
    seconds,
    // actions
    startClock: () => {
      batch(() => {
        setTurnOn(true)
        getTime()
      })
    },
  }
}

export function GlobalDemo() {
  const store = globalStoreDemo()
  return (
    <HStack gap="md" w="3/4">
      <Button onClick={store.startClock}>Start</Button>
      <ReactiveText data={store.time} />
    </HStack>
  )
}
Overview content
Features content
Pricing content
Toolbar
Footer
Kewl
Kewl
Kewl
Kewl
0

Render Count:

1
{
  "id": "7a08f556-fb99-4a62-8987-916636411561",
  "name": "User 7a08f556-fb99-4a62-8987-916636411561"
}