WebSocket-first data framework for SolidJS

Stop making your users wait for data they already have.

0ms revisit · 2.8KB gzipped · WebSocket-first

npm install @uikode/tide Click to copy
tide-demo.tsx

// First visit: skeleton shimmer

Revisit: renders from cache instantly

// Revisit: 0ms from sessionStorage

🔷 100% TypeScript 📐 2.8KB gzipped 🧩 Zero dependencies ⚖️ MIT View source →
~5.4× smaller than TanStack+persist (measured, gzip) ⚡ 0ms revisit

The Problem

You're building a real-time dashboard. API calls take 1–9 seconds. Every navigation = blank page → spinner → wait → render. Your data was RIGHT THERE 2 seconds ago.

/api/dashboard1,490× faster
Before
8.9s
After
6ms
/api/agentic/tooling350× faster
Before
5.6s
After
16ms
/api/stack/status833× faster
Before
5.0s
After
6ms
/api/gateways914× faster
Before
1.8s
After
2ms
/api/agentic/summary20× faster
Before
200ms
After
10ms
/api/announcements faster
Before
50ms
After
8ms
/api/daemon/status2.5× faster
Before
10ms
After
4ms

7

Endpoints optimized

<25ms

All responses after cache

1,490×

Fastest improvement

"Your data was RIGHT THERE. Why wait again?"

See the difference

Without Tide
Navigate
typical spinner load (illustrative)Total: ~850ms
vs
With Tide
Navigate
2.8KB totalTotal: 0ms (perceived)

How Tide Works

5 layers, 1 hook. Zero config.

Synchronous hydration before first paint — instant render on revisit

sessionStorage
// Layer 1: sessionStorage cache (persist: true)
const cached = readCache(key, cacheTime)
if (cached) {
  setData(cached)     // 0ms — instant render
  setStale(true)      // mark as potentially stale
  if (hashCompare) lastHash = contentHash(cached)
}

Server pushes updates via WS broadcast every 5s — reactive DOM update

WebSocket Push
// Layer 2: WebSocket push (wsPath dot-notation)
createEffect(() => {
  const msg = wsData()
  const extracted = getByPath(msg, opts.wsPath)
  if (hashCompare && contentHash(extracted) === lastHash)
    return  // identical — skip re-render
  setData(extracted)  // 12ms from server to DOM
})

Server-side cache returns in 2ms (dashboard) to 16ms (complex queries)

HTTP SWR Fetch
// Layer 3: HTTP SWR fetch (url shorthand + ETag)
const fresh = await fetchWithRetry(fetcher, retries, signal)
if (fresh === NOT_MODIFIED) return  // 304 — no update
if (hashCompare && contentHash(fresh) === lastHash)
  return  // content identical — skip DOM update
setData(fresh)
writeCache(key, fresh)

Exponential 1s/2s/4s recovery, abort stale requests on re-fetch

Retry + Abort
// Layer 4: Retry + Abort + Dedup
const controller = new AbortController()
const fetchFn = opts.dedupe !== false
  ? () => deduped(key, doFetch)  // prevent parallel fetches
  : doFetch
// Exponential backoff on WS: 1s → 2s → 4s → ... → 30s max
// Heartbeat ping every 25s keeps connection alive

Update UI before server confirms — rollback on failure

Optimistic UI
// Layer 5: Visibility + enabled() toggle
if (document.hidden) {
  stopPolling()  // save bandwidth when tab hidden
} else if (awayTime > 30_000 && refetchOnFocus) {
  refresh()      // re-validate after 30s away
}
// enabled: () => isLoggedIn() — reactive toggle
if (opts.enabled?.() === false) stopPolling()

Tap a layer to explore ↑

How Tide Compares

TanStack Query is great for REST. Tide is built for live data.

TanStack Query

API response (server cache) N/A
Revisit render ~80ms
WebSocket-first
sessionStorage persist
Server cache convention
Bundle size 15.5KB

solid-swr

API response (server cache) N/A
Revisit render ~100ms
WebSocket-first
sessionStorage persist
Server cache convention
Bundle size 1.33KB

@uikode/tide ✨

API response (server cache) 2ms
Revisit render 0ms
WebSocket-first
sessionStorage persist
Server cache convention
Bundle size 2.8KB

"TanStack polls. Tide pushes. When your data changes 10x/second, you don't want to ask — you want to be told."

"Why not just add WebSocket to TanStack?" →

🔄 Already using TanStack Query?

Before — TanStack
import { createQuery } from '@tanstack/solid-query'

const q = createQuery(() => ({
queryKey: ['dashboard'],
queryFn: fetchDashboard,
}))
After — Tide
import { createTide } from '@uikode/tide'

const d = createTide({
key: 'dashboard',
fetcher: ({ signal }) => fetchDashboard(signal),
})
Read Migration Guide →

5 minutes. Zero breaking changes.

See It In Action

example.tsx
import { createTide } from "@uikode/tide"

// v1.1: URL shorthand — no fetcher needed!
const users = createTide<User[]>({
  key: "users",
  url: "/api/users",         // auto-creates fetcher
  transform: (raw) => raw.data,  // optional transform
  staleTime: 30_000,
  cacheTime: 300_000,
})

// Revisit? Renders instantly from sessionStorage.

Real Production Benchmarks

From ACS Dashboard — 11 pages, 12 Hermes gateways, Go backend, WS broadcast every 5s

Revisit render (sessionStorage)0ms
WS → DOM update0ms
First paint (server cache warm)0ms
First paint (cold, no cache)0ms
Bundle size (gzipped, core+skeleton)0KB

vs TanStack Query

Revisit: ~80ms

Bundle: 15.5KB (+persist)

vs solid-swr

Revisit: ~100ms

Bundle: 1.33KB

@uikode/tide

Revisit: 0ms

Bundle: 2.8KB

ACS Dashboard powered by Tide — 0.5ms cache hit, 1ms TTFB, 16ms full load from sessionStorage
Production ACS Dashboard — Tide renders data from sessionStorage in 0.5ms before API responds

API Response Times (Server Cache)

Endpoint Before After Speed
/api/dashboard8,941ms6ms1,490×
/api/agentic/tooling5,609ms16ms350×
/api/stack/status5,000ms6ms833×
/api/gateways1,828ms2ms914×
/api/agentic/summary200ms10ms20×
/api/announcements50ms8ms
/api/daemon/status10ms4ms2.5×

All endpoints < 25ms after server-side cache warm

Test environment: Windows 11, Go binary, 12 Hermes gateways, 26 skills, 279 kanban tasks, 10 API providers, WS broadcast every 5s. Measured via CloakBrowser automated testing on localhost.

⚡ Running in 60 Seconds

Install
npm install @uikode/tide solid-js
Wrap your app
import { TideProvider } from '@uikode/tide'

function Root() {
return (
<TideProvider ws={{ url: 'wss://your-server/ws' }}>
<App />
</TideProvider>
)
}
Use anywhere
import { createTide } from '@uikode/tide'

const todos = createTide({
key: 'todos',
fetcher: ({ signal }) =>
fetch('/api/todos', { signal }).then((r) => r.json()),
ws: (msg) => (msg.type === 'todos' ? msg.data : null),
})

That's it. WebSocket, caching, reconnection, skeleton — all handled.

12 Problems Solved in 2.8KB

🌊

WebSocket-first

Push primary, poll fallback

Instant revisit

sessionStorage hydration (0ms)

💀

Skeleton system

Layout-matched shimmer components

🔄

Stale-while-revalidate

Show data, refresh in background

🎯

Prefetch on hover

Warm cache before navigation

🔁

Retry with backoff

Exponential 1s/2s/4s recovery

🛑

Request dedup

1 inflight request per key

Abort on re-fetch

Race-free data fetching

👁️

Visibility pause

Stop polling when tab hidden

🔌

Offline resilient

Cache serves indefinitely offline

📊

Dev logging

Hit/miss/timing in console

🪶

< 3KB gzipped

Zero dependencies, solid-js peer

What Teams Build With Tide

📊

Dashboards

Admin panels, monitoring, analytics

💬

Chat/Collab

Messaging, live editing, multiplayer

📈

Trading

Financial data, order books, price feeds

🤖

IoT/Devices

Sensor streams, fleet management

🎮

Game state

Lobby status, player state

🔧

Dev tools

Log viewers, process monitors

Frequently Asked Questions

Does Tide work with SSR / SSG?

Yes. Tide hydrates from sessionStorage on client. Server renders skeleton/fallback. Zero hydration mismatch.

Can I use Tide with React or Vue?

Tide is SolidJS-only by design. SolidJS signals enable 0-cost reactive subscriptions. We chose depth over breadth.

How does bundle size compare to TanStack Query?

Tide: 2.8KB gzipped (core+skeleton), WebSocket transport included. TanStack Query + persist plugin (parity for instant revisit): 15.5KB — Tide is ~5.4x smaller. Note: solid-swr is smaller still (1.33KB) but ships no WebSocket, persistence, or skeleton system.

Is Tide production-ready?

Tide is at v1.1.0 (published June 2026) and powers the ACS dashboard (11 pages, 12 gateways) in real use. Benchmarks on this page come from that deployment. It is young with low adoption so far — evaluate accordingly.

Why not just add a WebSocket adapter to TanStack Query?

TanStack was designed HTTP-first. Bolting WS on gives you double caching, no built-in reconnection, and loses stale-while-revalidate on transport switch. Tide is WS-first from line 1.

What happens when the WebSocket disconnects?

Automatic exponential backoff (1s/2s/4s) + HTTP fallback. UI stays interactive with stale cache, updates when connection restores.

How do I migrate from TanStack Query?

Most cases: swap createQuery for createTide, swap the provider. See the Migration Guide.

See the full Migration Guide →