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

Using Signals

Learn how to use signals within the React render context.

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

Usage

When you want to create a signal within the React render context which triggers re-renders.

There are two ways to read a signal from useSignal:

  1. Using the non-accessor value
  2. Combining the Accessor with ReactiveText for fine-grained reactivity

Basic State

useSignal is a more reliable version of useState that includes an additional Accessor as the third Tuple item.

Reading State

There are two ways to read the signal value based on your preferred method:

  1. Using the first value: the latest accessor result
  2. Using the third value: the pure accessor function

Both options are valid and more performant than native React state. However, only the pure accessor can be passed into ReactiveText.

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.

The second value of the Tuple is the Setter function.

The setter API is the exact same as the createSignal Setter and accepts any type of setting value or callback.

useSignal vs. useState: The "Stale Closure" Killer

React's useState traps values in closures. This is the root cause of 90% of React bugs. If you use useState inside a setInterval, an event listener, or an async fetch block, it will read the old, stale value from when the component first rendered.

To fix it, you have to juggle useRef and complex useEffect dependency arrays.

useSignal completely destroys this problem because it returns a getter Accessor function as the third option in the Tuple:

Separation of State and Lifecycle

With useState, the state is glued to the component. If the component unmounts, the state dies.

With useSignal, the state lives in the external reactive graph, and useRead just "peeks" at it. This means you can trigger complex business logic (createEffect, createComputed) completely outside of React's render cycle.

If setLocal updates the signal, it instantly triggers any pure-JS createEffect you have listening to it, synchronously, before React even begins its slow VDOM render phase.

API

useSignal accepts the following options:

ParamsRequiredDescription
initialValuefalseThe initial value to set for the accessor.

Return

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

IndexTypeDescription
0TThe latest value of the accessor.
1Setter<T>A function to update the signal value.
2Accessor<T>A function that returns the latest value when called.

0

0

Render Count: 1

Copy
'use client'

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

export function UseDemo() {
  const store = useStore(createRenderStore)
  const [count, setCount, getCount] = useSignal<number>(0)

  const increment = () => setCount(count + 1)

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

  store.trackRenders()

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

        <Text>{count}</Text>
        <ReactiveText data={getCount} />
      </HStack>

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

0

0

Render Count: 1

Copy

0

0

Render Count: 1

Copy
'use client'

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

export function UseDemo() {
  const store = useStore(createRenderStore)
  const [count, setCount, getCount] = useSignal<number>(0)

  const increment = () => setCount(count + 1)

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

  store.trackRenders()

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

        <Text>{count}</Text>
        <ReactiveText data={getCount} />
      </HStack>

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

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

export function UseDemo() {
  const store = useStore(createRenderStore)
  const [count, setCount, getCount] = useSignal<number>(0)

  const increment = () => setCount(count + 1)

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

  store.trackRenders()

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

        <Text>{count}</Text>
        <ReactiveText data={getCount} />
      </HStack>

      <HStack gap="md" w="full">
        <Text>
          Render Count: <ReactiveText data={store.renderCount} />
        </Text>
      </HStack>
    </HStack>
  )
}
import { useSignal } from '@cerberus/signals'

export function AccessorDemo() {
  const [_local, _setLocal, getLocal] = useSignal('hello')

  // Even if this runs 10 minutes from now, it will ALWAYS have the
  // absolute latest state, with zero dependency arrays needed.
  setTimeout(() => {
    console.log(getLocal())
  }, 600000)

  return null
}

On this page

  • Usage
  • Basic State
  • Reading State
  • Setting State
  • `useSignal` vs. `useState`: The "Stale Closure" Killer
  • Separation of State and Lifecycle
  • API
  • Return
  • Edit this page on Github