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 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 JSX:

  1. ReactiveText (recommended) for fine-grained reactivity
  2. useRead hook for reference consumption within JSX

Basic State

createSignal allows you to manage state outside of the React render context.

Reading State

The easiest way to get the current value of primitive-based signal is the ReactiveText component (which provides fine-grained reactivity).

When you need to reference a signal in any JSX-related context (e.g., Show or For) use the useRead hook to obtain the value.

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, Stack } from '@/styled-system/jsx'
import { Button, Text } from '@cerberus/react'
import { ReactiveText } from '@cerberus/signals'
import { useEffect } from 'react'
import { createRenderStore } from '../render-store'

const store = createRenderStore()

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

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

  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
'use client'

import { DecorativeBox } from '@/app/components/decorative-box'
import { HStack, Stack } from '@/styled-system/jsx'
import { Button, For, Text } from '@cerberus/react'
import {
  createComputed,
  ReactiveText,
  useRead,
  useStore,
} from '@cerberus/signals'
import { useEffect } from 'react'
import { createRenderStore } from '../render-store'

const store = createRenderStore()

export function ReadDemo() {
  // You can directly read signals outside of JSX
  const increment = () => store.setCount(store.count() + 1)
  const getCount = () => alert(store.count())

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

  store.trackRenders()

  return (
    <Stack gap="md" w="3/4">
      <HStack justify="space-between" w="full">
        <Stack>
          <Button onClick={increment} size="sm">
            Increment
          </Button>
          <Button onClick={getCount} size="sm">
            Get count
          </Button>
        </Stack>

        <Stack>
          <HStack gap="md">
            <Text>Count:</Text>
            <ReactiveText data={store.count} />
          </HStack>
          <HStack gap="md">
            <Text>Render Count:</Text>
            <ReactiveText data={store.renderCount} />
          </HStack>
        </Stack>
      </HStack>

      <ReactiveList />
    </Stack>
  )
}

function ReactiveList() {
  const listStore = useStore(createRenderStore)

  // useRead lives within the React-render scope because it's a hook.
  // This allows the For to know when to re-render to show the updated count.
  const count = useRead(store.count)
  const items = createComputed(() => {
    return Array.from({ length: count }, (_, i) => i + 1)
  })

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

  listStore.trackRenders()

  return (
    <Stack w="full">
      <HStack gap="md" h="2rem" overflowX="auto" w="full">
        <For each={items()}>
          {(item) => (
            <DecorativeBox p="sm" w="fit-content">
              {item}
            </DecorativeBox>
          )}
        </For>
      </HStack>

      <Text as="small" textStyle="sm">
        List Renders: <ReactiveText data={listStore.renderCount} />
      </Text>
    </Stack>
  )
}
Copy
'use client'

import { HStack, Stack } from '@/styled-system/jsx'
import { Add, Subtract } from '@carbon/icons-react'
import { Group, IconButton, Text } from '@cerberus/react'
import { createComputed, createSignal, ReactiveText } from '@cerberus/signals'
import { createRenderStore } from '../render-store'
import { useEffect } from 'react'

const store = createRenderStore()

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

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

  store.trackRenders()

  return (
    <Stack direction="column" gap="md" w="3/4">
      <HStack justify="space-between" w="full">
        <Group layout="attached">
          <IconButton ariaLabel="Increment" onClick={increment} usage="filled">
            <Add />
          </IconButton>
          <IconButton
            ariaLabel="Decrement"
            onClick={decrement}
            palette="danger"
            usage="filled"
          >
            <Subtract />
          </IconButton>
        </Group>
        <Text>
          Renders: <ReactiveText data={store.renderCount} />
        </Text>
      </HStack>

      <Text userSelect="none">
        <ReactiveText data={result} />
      </Text>
    </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

Count: 0

Render Count: 1

Count:

0

Render Count:

1
List Renders: 1

Renders: 1

1

0

On this page

  • Usage
  • Basic State
  • Reading State
  • Setting State
  • Objects and Arrays
  • Stores
  • API
  • Return
  • Edit this page on Github