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...

Signals Reactivity

Learn the fundamentals of fine-grained reactivity with signals.

  • source

Introduction

Reactivity powers the interactivity in React applications. This programming paradigm refers to a system's ability to respond to changes in data or state automatically.

In React, this is false positive natively built into the JSX render engine. This means, when state changes, React "re-renders" any component that either defines or consumes it. Ultimately, the "reactivity" is just a component function being called over and over (e.g., false positive).

Cerberus Signals remove that constraint and provides a way to control the define and consume state without causing re-renders when updates happen (e.g., true reactivity).

Primitives vs. Hooks

Cerberus Signals comes with two flavors of APIs: Primitives and Hooks.

Each API type will yield an intentional different result to keep the library flexible where you may need in your application.

Note

We recommend defaulting to primtives. They are more flexible and provide fine-grained outcomes.

Primitives

Primitives are functions that can be called in any location and scope.

It doesn't matter if you are using it on the global scope, in your component, in a callback, React effect, or even a Node script. There are no boundaries to primitives.

Primitives live outside of the React scope and do not trigger component re-renders.

Hooks

Hooks are React functions that are restricted to a component scope.

You know the rules: top level only - React knows about it.

Hooks live inside of the React scope and do trigger component re-renders.

This example shows how both the primitve and hook APIs affect a component render-cycle:


Here the Demo component will re-render only on updates. However, the ReactiveText component will never re-render on updates to ensure fine-grained reactivity.

This is just one of many ways to achieve fine-grained reactivity with Cerberus Signals.

Reactive Principles

There are two core elements in a reactive system: Signals and Subscribers.

Signals

Signals serve as core elements in reactive systems, playing an important role in data management and system responsiveness. They are responsible for storing and managing data, as well as triggering updates across the system.

This is done through the use of accessors (a.k.a. getters) and setters.


  • Accessor: A function responsible for getting the current value of the signal. You call a getter to access the data stored in a signal within a component.
  • Setter: The function used to modify a signal's value. To trigger reactive updates across an application, you call a setter to update the value of a signal.

There are two ways to create signals:

  • createSignal: create signals outside of components in any scenario
  • useSignal: create signals inside of components

Subscribers

Subscribers are the other core element in reactive systems. They are responsible for tracking changes in signals and updating the system accordingly. They are automated responders that keep the system up-to-date with the latest data changes.

Most importantly: this lives outside of the React render engine

This enables Cerberus Signals to allow creating global, local, external/local component state updates without affecting the VDOM renders.

Subscribers work based on two main actions:

  • Observation: At their core, subscribers observe signals. This keeps the subscriber primed to pick up on any changes to the signal they are tracking.
  • Response: When a signal changes, the subscriber is notified. This triggers the subscriber to respond to the change in the signal. This can involve tasks like updating the UI or calling external functions.

There are multiple APIs that subscribe to signals:

  • createEffect: watch for state updates without affecting the React lifecycle
  • createComputed: compute a new value based on other signals.
  • batch: batch signal updates
  • Any query or query related API

Synchronous Reactivity

Synchronous reactivity is Cerberus' default reactivity mode, where a system responds to changes in a direct and linear fashion. When a signal changes, any corresponding subscribers are immediately updated in an ordered manner.

With synchronous reactivity, the system is able to respond to changes in a predictable manner. This is useful in scenarios where the order of updates is important. For example, if a subscriber depends on another signal, it is important that the subscriber is updated after the signal it depends on.


In this example, the double signal will always be updated after count due to synchronous reactivity. This ensures that double is always up-to-date with the latest value of count.

Nesting Signals

Because signals are subscription-based, it opens the doors to news ways of thinking about state management. For example, you could nest Signals if you wanted to. Everything is subscribed vs. React state which just updates because a function is re-run from top to bottom (fake reactivity).

Note

We do not recommend using this pattern. This is just an informative note to show that Signals are more powerful that React state. It would be better to use other primitives devoted to this like createComputed.

Copy
'use client'

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

const store = createRenderStore()

export function BasicDemo() {
  const [local, setLocal] = useSignal<string>('hello')

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

  store.trackRenders()

  return (
    <HStack justify="space-between" w="1/2">
      <Stack>
        <Button size="sm" onClick={() => store.setCount((prev) => prev + 1)}>
          Increment
        </Button>
        <Button
          size="sm"
          onClick={() =>
            setLocal((prev) => {
              return prev === 'hello' ? 'world' : 'hello'
            })
          }
        >
          Set Local
        </Button>
      </Stack>

      <Stack userSelect="none">
        <Text>Local: {local}</Text>

        <Text>
          {/* Not re-rendered - only the html text value is updated */}
          Count: <ReactiveText data={store.count} />
        </Text>

        <Text>
          Renders: <ReactiveText data={store.renderCount} />
        </Text>
      </Stack>
    </HStack>
  )
}
Copy
'use client'

import { Stack } from '@/styled-system/jsx'
import { Button, ButtonGroup } from '@cerberus/react'
import { useSignal } from '@cerberus/signals'

export function NestingDemo() {
  const [count, setCount] = useSignal<number>(0)
  const [data, setData] = useSignal({
    count,
    unique: 0,
  })
  const [final, setFinal] = useSignal({
    data,
    finalCount: 0,
  })

  return (
    <Stack gap="lg" w="3/4">
      <pre>
        <code>{JSON.stringify(final, null, 2)}</code>
      </pre>

      <ButtonGroup>
        <Button onClick={() => setCount(count + 1)}>Increment</Button>
        <Button
          onClick={() => setData((prev) => ({ ...prev, unique: prev.unique + 1 }))}
        >
          Increment Unique
        </Button>
        <Button
          onClick={() =>
            setFinal((prev) => ({ ...prev, finalCount: prev.finalCount + 1 }))
          }
        >
          Increment Final
        </Button>
      </ButtonGroup>
    </Stack>
  )
}
import { createSignal } from '@cerberus/signals'

const [_count, _setCount] = createSignal<number>(0)
//     ^ getter  ^ setter
import { createEffect, createSignal } from '@cerberus/signals'

const [count, _setCount] = createSignal(0)
const [_double, setDouble] = createSignal(0)

createEffect(() => {
  setDouble(count() * 2)
})

Local: hello

Count: 0

Renders: 1

{
  "data": {
    "count": 0,
    "unique": 0
  },
  "finalCount": 0
}

On this page

  • Introduction
  • Primitives vs. Hooks
  • Reactive Principles
  • Signals
  • Subscribers
  • Synchronous Reactivity
  • Nesting Signals
  • Edit this page on Github