DocsBlog
  • 1.1.2

  • light

    dark

    system

    Switch mode
  • Cerberus

    Acheron

    Elysium

Get Started
Components
Data Grid
Signals
Styling
Theming

Get Started

OverviewReactivityData FetchingStores

Primitives

createSignalcreateQuerycreateMutationcreateComputedcreateEffectcreateStoreContextbatch

Hooks

useQueryuseMutationuseReaduseSignal

Components

ReactiveText

On this page

  • Edit this page on Github

Creating Signals

Learn how to create fine-grained signals in React.

  • source
import { createSignal } from '@cerberus/signals'

Usage

When you want to create a signal outside of a component context. This can be used globally, in a store, or any other way your purpose needs.

There are two ways to read a global signal within a component:

  1. ReactiveText (recommended) for fine-grained reactivity
  2. useRead hook for manual consumption

Basic State

createSignal works just like useSignal on a non-component scale.

Reading State

The easiest way to read the state of primitive-based signal is the ReactiveText component which provided fine-grained reactivity control.

When you don't care about fine-grained reactivity, you can use the useRead hook.

Note

We always recommend utilizing ReactiveText over useRead to provide better control over React rendering in addition to a cleaner DX.

Setting State

With Cerberus Signals, how you set the state no longer matters. You are free to use mutation or immutability. Both results will yield the same high performant and reliable reactivity.

In this example we utilize the createComputed primitive to create a reactive signal which provides a computational value. This is similar to the native React useMemo hook.

Note

Notice how the createComputed primitive doesn't require a dependency Array? In Cerberus Signals, we auto-detect signals so you don't have to waste time declaring.

Objects and Arrays

When storing Objects or Arrays (any non-Primitiva value) in a signal, we recommend using immutable updates when setting new values. Doing so will ensure the best performance outcome since the graph uses strict equality (!==) to detect changes.

Stores

You can create global stores to read and manage state across your entire application, or simply devote to a single feature (this is how the Data Grid is designed).

This allows you to have an "multi-signal" and action based solution that will both improve React rendering performance and code scalability.

API

createSignal accepts the following options:

ParamsRequiredDescription
initialValuefalseThe initial value to set for the accessor.

Return

createSignal returns a SignalTuple<T> Array of the following values:

IndexTypeDescription
0Accessor<T>A function that returns the latest value when called.
1Setter<T>A function to update the signal value.
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>
  )
}
Copy
'use client'

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

function createReadStore() {
  const [count, setCount] = createSignal<number>(0)
  const [readRenderCount, setReadRenderCount] = createSignal<number>(0)
  return {
    count,
    readRenderCount,
    increment: () => setCount(count() + 1),
    updateRenderCount: () => setReadRenderCount(readRenderCount() + 1),
  }
}

/**
 * This entire component will re-render when the count changes
 * because it uses useRead.
 *
 * However any ReactiveText child componens will **never** re-render.
 */
export function ReadDemo() {
  const store = createReadStore()
  const countVal = useRead(() => store.count())

  const increment = () => store.increment()

  store.updateRenderCount()

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

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

import { Stack } from '@/styled-system/jsx'
import { Add, Subtract } from '@carbon/icons-react'
import { Group, IconButton } from '@cerberus/react'
import { createComputed, createSignal, ReactiveText } from '@cerberus/signals'

const [count, setCount] = createSignal<number[]>([1])
const result = createComputed<string>(() => count().join(', '))

export function StateDemo() {
  // There is no difference between these two implementations
  // They both work correctly and efficiently
  const increment = () => setCount(count().concat([1]))

  const decrement = () => setCount((prev) => prev.slice(0, -1))

  return (
    <Stack direction="column" gap="md" w="3/4">
      <ReactiveText data={result} />

      <Group layout="attached">
        <IconButton ariaLabel="Increment" onClick={increment} usage="filled">
          <Add />
        </IconButton>
        <IconButton ariaLabel="Decrement" onClick={decrement} palette="danger" usage="filled">
          <Subtract />
        </IconButton>
      </Group>
    </Stack>
  )
}
Copy
'use client'

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

type CounterStore = {
  count: Accessor<number>
  decrement: () => void
  increment: () => void
}

function createCounter(): CounterStore {
  const [count, setCount] = createSignal<number>(0)
  return {
    count,
    decrement: () => setCount(count() - 1),
    increment: () => setCount(count() + 1),
  }
}

export function StoreDemo() {
  const store = createCounter()

  return (
    <HStack gap="md" w="3/4">
      <ReactiveText data={store.count} />

      <ButtonGroup>
        <Button onClick={store.decrement}>-</Button>
        <Button onClick={store.increment}>+</Button>
      </ButtonGroup>
    </HStack>
  )
}
import { createSignal } from '@cerberus/signals'

const [myObj, setMyObj] = createSignal({})
const [myArr, setMyArr] = createSignal<number[]>([])

// Object immutable updates
setMyObj((prev) => ({ ...prev, one: 1 }))
setMyObj({ ...myObj(), two: 2 })

// Array immutable updates
setMyArr((prev) => [...prev, 1])
setMyArr(myArr().with(1, 2))
setMyArr(myArr().map((val) => val + 1))
// ...there are a ton of immutalbe Array methods
0

Render Count:

1

0

Render Count:

1
1
0