DocsBlog

Get Started

Components

Data Grid

Signals

Styling

Theming

↑↓Navigate
↵Select
EscClose
  • 1.8.0

  • Day

    Night

    Preview

    Switch mode
  • cerberus

    acheron

    elysium

    oceanus

Get Started
Components
Data Grid
Signals
Styling
Theming

Get started

OverviewReactivityData FetchingGlobal Stores

Primitives

Creating SignalsCreating QueriesCreating MutationsComputing Signal ValuesCreating EffectsContextual Signal StoresBatch UpdatesCleanup EffectsUntrack Signals

Hooks

Using QueriesUsing MutationsReading Primitive SignalsUsing SignalsUsing Store Instances

Components

Reactive Text

Creating Queries

Learn how to create cached queries in React.

  • source
View as Markdown
Open this page in Markdown
Anthropic
Open in Claude
Ask questions about this page
OpenAI
Open in ChatGPT
Ask questions about this page
import { createQuery, useQuery } from '@cerberus/signals'

Usage

createQuery allows you define a fetcher factory that is auto-fetched and cached with or without an Accessor value. This creates a streamlined and high-performant way to fetch data within your application with less effort.

The value of a query is inteded to be consumed by useQuery.

A query is just a fancy wrapper that subscribes to the signals Observer. This means that it can be used for any scenario that is Promise-based - not just data-fetching. If you have a Promise, it can be a query.

See the features table to understand how to best utilize queries for each scenario you may have.

Note

Queries are compatible with React Suspense and Error Boundaries.

Creating Queries

Creating a query requires two things:

  1. A Promise to call when activated
  2. A unique key which is used to cache the result

Ultimately, when you create a query you are really just creating a factory that can be called where ever and whenever you are ready.

Loading example...

Using Queries

Depending on your goals will determine how to use the query. The following sections outline the different use cases depending on whether you are fetching data within a component or outside of one.

There are no restrictions to Cerberus Signal Primitives which means you can use them at any scope.

With Components

createQuery only defines a query factory and does not actually call it. To use the query in a component you must pass the returned value into the useQuery hook which re-runs when the arguments provided change.

const data = useQuery(myQuery(props.id))

The value returned is the result from calling the signal-tracked Accessor. This is equivalent to the return value from the useRead hook.

Without Components

There are two different patterns you can use depending on your fetching goals outside of components.

Pattern 1: Imperative Await (Standard Async)

If you just want to trigger the fetch and wait for the result in a standard async function, you can simply await the Promise attached to the pending state.

Loading example...

Pattern 2: Reactive Subscription (The Signal Way)

If you want a vanilla JS function to automatically react to the data whenever it arrives (or whenever it is invalidated and refetched in the background), you drop it into a createEffect.

This is incredibly useful for syncing data to vanilla DOM elements, Web Components, or external non-React stores.

Loading example...

Streaming Reponses (Async Generators)

Cerberus queries also support streaming data via Async Generators. This is powerful if you are using an LLM API or your local API supports data streaming.

Loading example...

Server Component Pattern

In an SSR environment, you execute the factory's raw fetcher directly, bypassing the reactive cache. Then, you pass that data down to your Client Components to "hydrate" or seed the Cerberus cache, ensuring the client doesn't double-fetch on mount.

This is the standard SSR pattern for React.

1. Server Component (Fetching)

Expose the raw, stateless fetcher function from your factory. You simply await it like a standard asynchronous function.

Loading example...

2. Client Component

On the client side, use the initialData property. If the Cerberus cache is empty, it will instantly seed the cache with the server's data, skipping the <Suspense> boundary entirely.

Loading example...

Optimistic Updates

Queries have a built-in optimistic store when combined with Mutations. This means the UI will update in "real-time" while the query will fetch in the background and eventually overwrite the optimistic store.

Mutations are only necessary if you are writing updates. If your query is only intended to be read-only, it will automagically fetch when the params change.

Features Table

FeatureWith SignalWithout Signal
auto-intial fetch:white_check_mark::white_check_mark:
auto-refresh fetch:white_check_mark::white_check_mark:
caching:white_check_mark::white_check_mark:
requires mutation:x::x:

API

createQuery accepts the following options:

ParamsRequiredDescription
Promise<any>trueThe Promise to call when activated.
keytrueA unique key used for caching queries.

Return

createQuery returns the value of the Promise along with some additional properties:

PropertyTypeDescription
keyUUIDA uniqe ID to be referenced in the mutation invalidate Array.
currentArgsArgsThe args passed into createQuery stored in a reference.

Note

The query.key is SHA generated from the contents of the query parameters. This means, if you have multiple signal-like queries, they should each have unique return values or else they will be synced together.

Typing

createQuery accepts two arguements to strictly type the return result <CacheNode, Args>. These are naturally deferred through our strict type chain. This means you don't need to worry about it outside of rare use cases.

However, we do recommend using this if you are creating a query that doesn't expect any arguments: createQuery<string, void>(...).

On this page

  • Usage
  • Creating Queries
  • Using Queries
    • With Components
    • Without Components
      • Pattern 1: Imperative Await (Standard Async)
      • Pattern 2: Reactive Subscription (The Signal Way)
  • Streaming Reponses (Async Generators)
  • Server Component Pattern
    • 1. Server Component (Fetching)
    • 2. Client Component
  • Optimistic Updates
  • Features Table
  • API
    • Return
    • Typing
Edit this page on Github
{
  "id": "16aad9b1-67e2-443b-bb09-df6cf0ee4f49",
  "name": "User 16aad9b1-67e2-443b-bb09-df6cf0ee4f49"
}
Copy
import { createQuery } from '@cerberus/react/signals'

const getUUID = createQuery(fetchUUID, 'queryUUID')

export async function main() {
  const state = getUUID(undefined)()

  if (state.status === 'pending' && state.promise) {
    try {
      // Await the underlying fetch directly
      const uuid = await state.promise
      console.log('Fetched successfully:', uuid)
    } catch (error) {
      console.error('Fetch failed:', error)
    }
  } else if (state.status === 'success') {
    // If it was already cached, just use it instantly
    console.log('Read from cache:', state.data)
  }
}

// API

type UUID = string

function fetchUUID(): Promise<UUID> {
  return new Promise<UUID>((resolve) => {
    setTimeout(() => {
      resolve(crypto.randomUUID())
    }, 500)
  })
}
Copy
import { createQuery, createEffect } from '@cerberus/react/signals'

const queryUUID = createQuery(fetchUUID, 'queryUUID')

export function main() {
  // 1. Get the reactive accessor for this specific query
  const getUUIDState = queryUUID({})

  // 2. This effect runs immediately, automatically tracking the accessor.
  // If the cache is empty, reading getUUIDState() triggers the background fetch!
  createEffect(() => {
    // Read the live signal state directly
    const state = getUUIDState()

    if (state.status === 'pending') {
      console.log('Loading UUID...')
    } else if (state.status === 'success') {
      console.log('Syncing UUID to external system:', state.data)
      // document.getElementById('my-span').innerText = state.data
    } else if (state.status === 'error') {
      console.error('Failed to load UUID:', state.error)
    }
  })
}

// API

type UUID = string
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))

async function fetchUUID(): Promise<UUID> {
  await delay(500)
  return crypto.randomUUID()
}
Copy
// app/users/[id]/page.tsx (Server Component)

import { UserProfile } from './client.demo'
import { queryUser } from './queries'

interface Props {
  params: Promise<{ id: string }>
}

export default async function UserPage({ params }: Props) {
  const { id } = await params
  // Bypass the reactive cache and execute the raw fetcher directly.
  // This is completely memory-safe for Node.js environments.
  const initialUserData = await queryUser.fetcher(id)

  if (!initialUserData) return null

  return (
    <main>
      {/* 2. Pass the fetched data to the Client Component */}
      <UserProfile id={id} initialData={initialUserData} />
    </main>
  )
}
Copy
'use client'

import { useQuery } from '@cerberus/react/signals'
import { queryUser } from './queries'
import { type User } from './db'

interface Props {
  id: string
  initialData: User
}

export function UserProfile(props: Props) {
  // 1. Cerberus sees `initialData`, instantly seeds the $O(1) Map,
  // and mounts the component with zero loading spinners or network waterfalls.
  const user = useQuery(queryUser(props.id), { initialData: props.initialData })

  if (!user) return null

  return <h1>{user.name}</h1>
}