API Reference

hydrate()

Reads every registered store's IndexedDB cache in parallel and dispatches whatever it finds — before any network request fires. This is what makes a repeat visit render instantly instead of showing a loading spinner.

Prerequisites
  • flux created via createFlux() — see api/create-flux
  • Stores already registered via register() — see api/register
1

Basic usage

flux.hydrate() reads IDB for every registered store and dispatches whatever's cached. In a Next.js or React app, FluxProvider calls this once on mount — you rarely need to call it yourself.
typescript
1// Most apps never call this directly — FluxProvider (flux-next / flux-react)
2// calls it once on mount and exposes the result via isHydrated / useFlux().
3await flux.hydrate()
4
5// Manual usage looks like this:
6const flux = createFlux({ adapter, license })
7await flux.hydrate()
8console.log(flux.isHydrated) // true
2

What happens per store

Every registration resolves independently into one of three outcomes, driven entirely by that store's own cached cachedAt and ttl.
typescript
1// For every registered store, hydrate() resolves one of three ways:
2
3// 1. Fresh — cachedAt is within ttl
4// -> dispatchToStore(reg, data) immediately, no onStale fired
5
6// 2. Expired — data exists but ttl has passed
7// -> dispatchToStore(reg, data) immediately (stale data shown instantly)
8// -> reg.onStale?.() fires right after, so you can trigger a refetch/toast
9
10// 3. No data — nothing cached yet for this key
11// -> store's loading state is left false, no dispatch
12// -> component waits for bootstrap() or the next realtime event
Stale is still shown instantly

An expired cache entry is still dispatched immediately — the UI never waits on a network round trip just because the TTL passed. onStale is your hook for kicking off a refetch or showing a subtle "updating..." indicator alongside the stale data.

3

Parallel reads, isolated failures

All registered stores are read via Promise.allSettled, not a sequential loop. If one store's IDB read throws — a corrupted envelope, a quota issue — every other store still hydrates normally.
typescript
1// Internally: hydrateStores() reads every registration's IDB key
2// via Promise.allSettled — not a sequential loop.
3
4const results = await Promise.allSettled(
5 registrations.map((reg) => idbGet(reg.idbKey, storagePrefix, isLru, scope, userId))
6)
7
8// One store's read throwing (corrupted envelope, quota issue, etc.)
9// never blocks or delays any other store from hydrating.
4

The 2000ms hard timeout

HydrationGate waits for both flux.hydrated and the service worker's ready signal, but never longer than 2000ms — on a first visit with no SW installed yet, the app renders unhydrated rather than hanging.
typescript
1// HydrationGate (flux-next / flux-react) waits for two things:
2// 1. flux.hydrated resolving
3// 2. window.FLUX_SW_READY resolving (service worker active)
4//
5// Whichever finishes last releases the gate — capped at 2000ms so the
6// app always renders even if IDB or the SW never resolve at all.
7
8<FluxProvider flux={flux}>
9 <HydrationGate>
10 <App />
11 </HydrationGate>
12</FluxProvider>
5

Scope-aware hydration

Hydration resolves each store's scope the same way bootstrap and dispatch do — reading the engine's live currentUserId to build the correct namespaced key for 'user'-scoped stores.
typescript
1// hydrateStores() reads the engine's live currentUserId and, for
2// every registration, resolves scope before calling idbGet:
3
4const scope = reg.scope ?? 'global'
5const key = resolveScopedKey(reg.idbKey, scope, scope === 'user' ? currentUserId : undefined)
6
7// A 'user'-scoped store with no active userId yet is treated as a
8// cold cache, not an error — loading clears and the store waits for
9// the user-scope bootstrap() call once auth resolves.
No active user isn't an error

A 'user'-scoped store hydrating before login isn't a failure case — it's treated as a cold cache. Loading clears and the store simply waits for the user-scope bootstrap() call once auth resolves. See core-concepts/multi-tenant-scoping.

6

LRU stores are skipped

ingestionType: 'LRU' registrations are never collectively hydrated at boot. The root idbKey only ever holds a heartbeat envelope used by the revalidation loop — never real data — so hydration explicitly skips it rather than dispatching that heartbeat as if it were the collection.
typescript
1// LRU registrations are skipped entirely during hydrateStores().
2// The root idbKey only ever holds the heartbeat envelope
3// ({ data: true, cachedAt, ttl }) — never real item data — so
4// forwarding it into the store would dispatch a boolean as if it
5// were the collection.
6//
7// Detail pages read their own item directly on mount instead:
8const { data } = await idbGet('blog_post_detail:my-slug', storagePrefix)
The consuming component reads its own item directly via idbGet on mount instead. Full behavior is covered in ingestion/lru.
7

Reading hydration state

flux.isHydrated is a read-only boolean on the engine. useFlux() mirrors it as hydrated for components that need to react without wiring their own gate.
typescript
1import { useFlux } from '@tsworldtech/flux-next'
2
3function Nav() {
4 const { hydrated, isBootstrapped } = useFlux()
5
6 if (!hydrated) return null // or a skeleton
7 return <nav>{/* ... */}</nav>
8}

For populating stores that had no cached data at all, see api/bootstrap. For the isolation model behind scope-aware hydration, see core-concepts/multi-tenant-scoping.