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

Global Stores

Learn the fundamentals of creating signal-based stores in React Signals.

  • source

Introduction

As your app grows you may find the need for a single-source of truth for managing signals in a scalable way. In Cerberus Signals you can achieve this by creating a store.

A store is simply a function that contains mutliple signals and actions which manage those signals. There are no restrictions for what a store is.

A store can use any primitive API in it. This means the potential is limitless for whatever you wish to achieve in a high-performant way that doesn't involve React controlling the scenario.

Global Stores

To create a global store, simply create a function that returns an Object. It can be anything and use any Cerberus Signals Primitive API within it.

Contextual Stores

To create a store that uses unique instances on a React app/component level, use the createStoreContext API.

React rules

Depending on the complexity and use case of your store, React may "get in the way" of your signal management when using stores within a component.

If you intend on using a store within a component you must follow one of the two designs to ensure React doesn't corrupt your hard work via the render cycles:

Pure Global Stores

If you have a store that can be called on the global scope (outside of a component), then you are good to go. Utilize useRead for iteration and ReactiveText for reading.

Local Component Store

If a store must live within a component, then you will need to take precautions to ensure React does not corrupt your store state via wrapping your store in useMemo.

Note

Wrapping your store in useMemo is only required if you are using the useRead hook to obtain the reactive value from a store. If your component is only calling actions or utilizing the ReactiveText component, then it is unneccessary (as shown in the demos in the prior sections).

Reactivity

Since stores are essentially a primitive that contain primitives, you will need to utilize the ReactiveText or useRead APIs in order to obtain the reactive values within the scope of any component.

When calling a store within a component and reading a value with useRead you must wrap your store in useMemo to ensure React does not corrupt the signal state.

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>
  )
}
Copy
'use client'

import { HStack, Square } 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'

function myStore() {
  const [count, setCount] = createSignal<number>(0)

  const multiplied = createComputed<number>(() => count() + count())

  return {
    count,
    multiplied,
    increment: () => setCount(count() + 1),
    decrement: () => setCount(count() - 1),
  }
}

// Call it globally - outside of the React component scope
const store = myStore()

export function PureDemo() {
  return (
    <HStack gap="lg" w="3/4">
      <Group>
        <IconButton ariaLabel="Decrement" onClick={store.decrement}>
          <Subtract />
        </IconButton>

        <Square>
          <ReactiveText data={store.count} />
        </Square>

        <IconButton ariaLabel="Increment" onClick={store.increment}>
          <Add />
        </IconButton>
      </Group>

      <Text>
        Multiplied: <ReactiveText data={store.multiplied} />
      </Text>
    </HStack>
  )
}
Copy
'use client'

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

type MyStore = {
  users: Accessor<string[]>
  selectedUser: Accessor<string | null>
  setSelectedUser: (user: string | null) => void
  addUser: (user: string) => void
}

function myStore(): MyStore {
  const [users, setUsers] = createSignal<string[]>([])
  const [selectedUser, setSelectedUser] = createSignal<string | null>(null)

  return {
    users,
    selectedUser,
    setSelectedUser: (user: string | null) => setSelectedUser(user),
    addUser: (newUser: string) => {
      setUsers([...users(), newUser])
    },
  }
}

const { StoreProvider, useStore } = createStoreContext<MyStore>()

// Components

export function BasicDemo() {
  return (
    <StoreProvider createStore={myStore}>
      <UserList />
    </StoreProvider>
  )
}

function UserList() {
  const store = useStore()
  const users = useRead(store.users)

  return (
    <Stack direction="column" gap="md" w="3/4">
      <Stack direction="column" gap="md" w="full">
        <Text>
          Selected: <ReactiveText data={store.selectedUser} />
        </Text>

        <Button
          size="sm"
          onClick={() => {
            store.addUser(crypto.randomUUID())
          }}
        >
          Add User
        </Button>
      </Stack>

      <cerberus.ul display="flex" flexDirection="column" gap="md" w="full">
        <For each={users}>
          {(user) => (
            <li key={user}>
              <HStack justify="space-between" w="full">
                {user}
                <Button size="sm" onClick={() => store.setSelectedUser(user)}>
                  Select
                </Button>
              </HStack>
            </li>
          )}
        </For>
      </cerberus.ul>
    </Stack>
  )
}
Copy
'use client'

import { HStack, Square } from '@/styled-system/jsx'
import { Add, Subtract } from '@carbon/icons-react'
import { For, Group, IconButton, Text } from '@cerberus/react'
import {
  createComputed,
  createSignal,
  ReactiveText,
  useRead,
} from '@cerberus/signals'
import { useMemo } from 'react'

function myStore() {
  const [count, setCount] = createSignal<number>(0)

  const multiplied = createComputed<number>(() => count() + count())

  return {
    count,
    multiplied,
    increment: () => setCount(count() + 1),
    decrement: () => setCount(count() - 1),
  }
}

export function LocalDemo() {
  // You only need to do this if you are using `useRead` for iteration
  const store = useMemo(() => myStore(), [])
  const count = useRead(store.count)

  return (
    <HStack gap="lg" w="3/4">
      <Group>
        <IconButton ariaLabel="Decrement" onClick={store.decrement}>
          <Subtract />
        </IconButton>

        <Square>
          <ReactiveText data={store.count} />
        </Square>

        <IconButton ariaLabel="Increment" onClick={store.increment}>
          <Add />
        </IconButton>
      </Group>

      <For each={Array.from({ length: count }, (_, i) => i)}>
        {(i) => <Text key={i}>{i}</Text>}
      </For>
    </HStack>
  )
}
0

Multiplied: 0

Selected:

    0