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: