Signals & Store

Davaux ships a fine-grained reactivity system for use in client islands. Import everything from davaux/client. On the server these are no-ops or return static values — they only activate in the browser.

createSignal

The fundamental reactive primitive. Returns a getter/setter pair:

import { createSignal } from 'davaux/client'

const [count, setCount] = createSignal(0)

count()         // read: 0
setCount(1)     // write
setCount(n => n + 1)  // updater function
count()         // read: 2

In JSX, pass the getter as a function to make that spot reactive:

// Non-reactive — renders once:
<span>{count()}</span>

// Reactive — updates when count changes:
<span>{() => count()}</span>

Typing signals

import type { Signal, ReadonlySignal } from 'davaux/client'

const [items, setItems] = createSignal<string[]>([])

// Pass a read-only signal to child components
function List({ items }: { items: ReadonlySignal<string[]> }) {
  return <ul>{() => items().map(i => <li>{i}</li>)}</ul>
}

createEffect

Runs a side effect whenever its reactive dependencies change. The effect tracks which signals are read during execution and re-runs when any of them update:

import { createSignal, createEffect } from 'davaux/client'

const [name, setName] = createSignal('Alice')

createEffect(() => {
  document.title = `Hello, ${name()}!`
  // Re-runs automatically whenever `name` changes
})

Effects run once immediately (on first execution) to establish their dependency set, then again whenever a tracked dependency changes.

createMemo

Creates a derived reactive value. Like createEffect, it tracks dependencies — but it returns a signal-like getter and memoizes the result (only recomputes when dependencies actually change):

import { createSignal, createMemo } from 'davaux/client'

const [firstName, setFirstName] = createSignal('John')
const [lastName,  setLastName]  = createSignal('Doe')

const fullName = createMemo(() => `${firstName()} ${lastName()}`)

// In JSX:
<h1>{() => fullName()}</h1>

Use createMemo when the derived computation is expensive or when many places depend on the same derived value.

untrack

Reads a signal without registering a dependency. Useful when you need a value inside an effect but don't want the effect to re-run when that value changes:

import { createSignal, createEffect, untrack } from 'davaux/client'

const [a, setA] = createSignal(1)
const [b, setB] = createSignal(10)

createEffect(() => {
  // This effect only re-runs when `a` changes
  const aVal = a()
  const bVal = untrack(() => b())  // read b without tracking
  console.log(aVal + bVal)
})

batch

Batches multiple signal writes into a single notification pass. Without batching, each set call triggers its own downstream update cycle:

import { createSignal, createEffect, batch } from 'davaux/client'

const [x, setX] = createSignal(0)
const [y, setY] = createSignal(0)

createEffect(() => console.log(x(), y()))

// Without batch: effect runs twice
setX(1)
setY(2)

// With batch: effect runs once after both updates
batch(() => {
  setX(3)
  setY(4)
})

onCleanup

Registers a teardown function tied to the current reactive scope. Called when the scope is destroyed or before the effect re-runs:

import { createSignal, createEffect, onCleanup } from 'davaux/client'

const [id, setId] = createSignal('room-1')

createEffect(() => {
  const roomId = id()
  const ws = new WebSocket(`wss://chat.example.com/${roomId}`)

  ws.onmessage = (e) => console.log(e.data)

  onCleanup(() => ws.close())
  // WebSocket is closed before the effect re-runs with a new id
})

createRoot

Creates a reactive scope that you control explicitly. Returns a dispose function to tear it down. Useful for managing reactive state outside of the normal component lifecycle:

import { createRoot, createSignal, createEffect } from 'davaux/client'

const dispose = createRoot((d) => {
  const [tick, setTick] = createSignal(0)

  const interval = setInterval(() => setTick(n => n + 1), 1000)

  createEffect(() => console.log('Tick:', tick()))

  return d  // return the dispose function
})

// Later, tear everything down:
dispose()

createStore

A reactive store for nested objects. Returns a [store, setStore] pair where store is a deeply reactive proxy:

import { createStore } from 'davaux/client'

const [state, setState] = createStore({
  user: { name: 'Alice', age: 30 },
  settings: { theme: 'dark', lang: 'en' },
  tags: ['ts', 'web'],
})

Reading store values

Read values directly — they are reactive when accessed inside effects or JSX:

<p>{() => state.user.name}</p>
<p>{() => state.settings.theme}</p>

Writing with path setters

Use dot-path strings or arrays to update deeply nested values without spreading:

// Top-level key
setState('user', { name: 'Bob', age: 25 })

// Nested key
setState('settings', 'theme', 'light')

// Updater function (receives previous value)
setState('user', 'age', (age) => age + 1)

// Array element
setState('tags', 0, 'typescript')

// Append to array
setState('tags', (tags) => [...tags, 'node'])

Sharing a store across islands

Module-level stores are shared across all islands on the page since they share a single bundle:

// src/islands/globalStore.ts
import { createStore } from 'davaux/client'

export const [appState, setAppState] = createStore({
  cartItems: [] as { id: string; qty: number }[],
  sidebarOpen: false,
})

Import appState and setAppState in any island — they all read from and write to the same reactive object.

createResource

Wraps an async fetcher in a reactive signal. The returned accessor has reactive .loading and .error properties so you can drive loading states directly in JSX.

import { createResource, Show } from 'davaux/client'

function PostList() {
  const [posts, { refetch }] = createResource(() =>
    fetch('/api/posts').then(r => r.json())
  )

  return (
    <div>
      <Show when={() => !posts.loading} fallback={<p>Loading…</p>}>
        <ul>{() => posts()?.map(p => <li>{p.title}</li>)}</ul>
      </Show>
      <Show when={() => !!posts.error}>
        <p>Error: {() => String(posts.error)}</p>
      </Show>
      <button onClick={refetch}>Refresh</button>
    </div>
  )
}

posts() returns the resolved value (or undefined while loading). posts.loading and posts.error are reactive — read them inside () => children or createEffect to track changes.

Reactive source

Pass a source signal as the first argument and a fetcher that receives its value as the second. The resource re-fetches automatically whenever the source changes. Returning false, null, or undefined from the source skips the fetch entirely.

import { createSignal, createResource, For, Show } from 'davaux/client'

function UserPosts() {
  const [userId, setUserId] = createSignal<number | null>(null)

  const [posts] = createResource(
    () => userId(),
    (id) => fetch(`/api/users/${id}/posts`).then(r => r.json()),
  )

  return (
    <div>
      <button onClick={() => setUserId(1)}>Load user 1</button>
      <button onClick={() => setUserId(2)}>Load user 2</button>
      <Show when={() => !posts.loading} fallback={<p>Loading…</p>}>
        <For each={posts} key={(p) => p.id}>
          {(post) => <p>{post.title}</p>}
        </For>
      </Show>
    </div>
  )
}

If multiple source changes arrive before a fetch completes, only the result from the latest request is used — stale responses are silently discarded.

Polling with refetchInterval

Pass refetchInterval (milliseconds) to poll automatically. The interval is cleared when the island's reactive scope is disposed:

const [data] = createResource(fetcher, { refetchInterval: 5000 })

createEventSource

Subscribes to a Server-Sent Events endpoint and returns the latest message as a signal. The connection is automatically closed when the reactive scope is disposed.

import { createEventSource } from 'davaux/client'

function LiveFeed() {
  const [message] = createEventSource('/api/events')

  return <p>Latest: {() => message() ?? '—'}</p>
}

message() starts as undefined and updates to the latest event's data string each time the server pushes an event.

Live example: theme toggle

The theme toggle in the sidebar of this page is a Davaux island — two primitives, no framework extras:

// src/islands/ThemeToggle.tsx
import { createEffect, createSignal } from 'davaux/client'

export default function ThemeToggle() {
  const initial =
    typeof document !== 'undefined'
      ? ((document.documentElement.getAttribute('data-theme') as 'dark' | 'light') ?? 'dark')
      : 'dark'

  const [theme, setTheme] = createSignal<'dark' | 'light'>(initial)

  createEffect(() => {
    if (typeof document === 'undefined') return
    document.documentElement.setAttribute('data-theme', theme())
    localStorage.setItem('davaux-theme', theme())
  })

  return (
    <button
      type="button"
      class="theme-toggle"
      aria-label="Toggle color theme"
      onClick={() => setTheme(theme() === 'dark' ? 'light' : 'dark')}
    >
      <span class="icon-light">☀ Light</span>
      <span class="icon-dark">☽ Dark</span>
    </button>
  )
}

createSignal holds the current theme string. createEffect watches it and writes to two external targets whenever it changes: data-theme on <html> (which CSS selector rules react to automatically) and localStorage (for persistence across page loads).

The button below is the live island — clicking it switches the theme for the whole page: