Examples

Real-world usage patterns for Tide v1.2.

URL shorthand

Replace verbose fetcher boilerplate with a simple url string. Built-in handler includes credentials, abort, and AuthError detection.

tsx
const gateways = createTide({
key: 'gateways',
url: '/api/gateways',
wsPath: 'data.hermes.gateways',
})

ETag / conditional fetch

Enable etag to send If-None-Match headers. On 304, Tide skips the update — zero bandwidth, zero re-render.

tsx
const tooling = createTide({
key: 'tooling',
url: '/api/agentic/tooling',
etag: true,
pollInterval: 15000,
})

Content deduplication

hashCompare uses djb2 hashing to skip reactive updates when data is identical — prevents unnecessary DOM reconciliation.

tsx
const stack = createTide({
key: 'stack',
url: '/api/stack/status',
hashCompare: true,
pollInterval: 5000,
})

Reactive key (dynamic routes)

Pass a getter function for key and url. Tide re-fetches automatically when either changes.

tsx
const [query, setQuery] = createSignal('react')

const results = createTide({
key: () => `search-${query()}`,
url: () => `/api/search?q=${query()}`,
})

Reactive key — filter/tab switching

When using reactive keys for tab or filter UIs, each key maintains its own cache entry. Switching tabs fetches fresh data (or hits cache if within cacheTime), and the loading state resets per key.

tsx
const [period, setPeriod] = createSignal<"today" | "24h" | "7d" | "30d">("today")

const chart = createTide<ChartResponse>({
key: () => `usage-chart-${period()}`,
url: () => `/api/usage/chart?period=${period()}`,
staleTime: 30_000,
cacheTime: 300_000,
persist: true,
})

// Each period gets its own cache entry:
// - "usage-chart-today" → cached separately
// - "usage-chart-24h" → cached separately

// UI: skeleton while loading, instant on cache hit
return (
<div>
<div class="flex gap-1">
<For each={["today", "24h", "7d", "30d"] as const}>
{(p) => (
<button
class={period() === p ? "active" : ""}
onClick={() => setPeriod(p)}
>
{p}
</button>
)}
</For>
</div>

<Show when={!chart.loading() && chart.data()} fallback={<ChartSkeleton />}>
<Chart data={chart.data()!} />
</Show>
</div>
)
Key behaviors: first visit = loading + skeleton, revisit within cacheTime = instant (0ms) from sessionStorage, revisit after cacheTime = fresh fetch. WebSocket pushes update whichever key matches the current reactive value. persist: true survives page refresh.

Conditional fetch (enabled)

Use enabled to pause all activity (fetch, poll, WS) until a condition is met.

tsx
const [loggedIn, setLoggedIn] = createSignal(false)

const data = createTide({
key: 'private',
url: '/api/private',
enabled: () => loggedIn(),
})

wsPath shorthand

Use wsPath instead of a ws function for simple dot-notation extraction from WebSocket messages.

tsx
// Before (v1.0)
ws: (msg) => msg?.data?.stack ?? null

// After (current — standard)
wsPath: 'data.stack'

WS backoff + heartbeat

tsx
<TideProvider
ws={{ url: 'wss://server/ws' }}
reconnect={{ baseMs: 1000, maxMs: 30000 }}
heartbeat={25000}
>
<App />
</TideProvider>

pause() / resume() — lifecycle control

NEW in v1.2.0. Use pause() before optimistic mutations to stop polling and abort inflight requests. Call resume() + await refresh() after the operation to re-sync with the server.

tsx
async function handleStart() {
tide.pause() // stop polling, prevent overwrite
tide.mutate(prev => ({ ...prev, status: 'starting' }))
await fetch('/api/start', { method: 'POST' })
tide.resume() // restart polling
await tide.refresh() // wait for fresh data
// UI now reflects real server state
}
Without pause(), Tide's polling interval can overwrite your optimistic state mid-operation. Always pair resume() with await refresh() to ensure data convergence before unlocking UI.