npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

qwik-swr

v0.2.0

Published

Lightweight Stale-While-Revalidate data fetching library for Qwik

Readme

qwik-swr

Stale-While-Revalidate data fetching library for Qwik.

Features

  • Qwik-native -- works with Signals, QRLs, and Resumability
  • Type-safe -- full type inference from key to fetcher to response
  • Separated status model -- status (data state) and fetchStatus (network state) never conflict
  • Freshness presets -- 6 built-in presets from volatile to static
  • Request deduplication -- concurrent calls with the same key share a single fetch
  • Auto revalidation -- on focus, reconnect, or interval
  • Retry with backoff -- configurable retry count and exponential backoff
  • Optimistic updates -- mutate$ with optional revalidation
  • SSR integration -- works with Qwik City's routeLoader$ via fallbackData
  • Global config -- SWRProvider for app-wide defaults
  • Cache API -- programmatic access to read, mutate, delete, prefetch, export/import
  • Cross-tab sync -- cache changes propagate across browser tabs via BroadcastChannel
  • Notification batching -- observer updates are batched per microtask/interval
  • Storage write batching -- IndexedDB/localStorage writes are coalesced
  • Lazy hydration -- on-demand cache loading for faster startup
  • Timer coordination -- shared timers for refreshInterval polling
  • Memory-aware GC -- maxEntries + deviceMemory-based adaptive limits
  • Real-time subscriptions -- WebSocket/SSE with auto-reconnect and cross-tab dedup

Install

npm install qwik-swr

Requires @builder.io/qwik >= 1.5.0 as a peer dependency.

Quick Start

import { component$, $ } from "@builder.io/qwik";
import { useSWR } from "qwik-swr";

export default component$(() => {
  const { data, error, isLoading } = useSWR<User[]>(
    "/api/users",
    $(async (ctx) => {
      const res = await fetch(ctx.rawKey, { signal: ctx.signal });
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      return res.json();
    }),
  );

  return (
    <div>
      {isLoading.value && <p>Loading...</p>}
      {error.value && <p>Error: {error.value.message}</p>}
      {data.value?.map((user) => (
        <p key={user.id}>{user.name}</p>
      ))}
    </div>
  );
});

Initialization

Call initSWR once at app startup to configure storage, cross-tab sync, batching, and GC.

import { initSWR } from "qwik-swr";
import { createIndexedDBStorage } from "qwik-swr/storage";

await initSWR({
  storage: createIndexedDBStorage(),
  sync: { enabled: true, channelName: "my-app" },
  batching: { notifyInterval: 0, storageFlushInterval: 50 },
  hydration: "lazy",
  gc: { maxEntries: 500, memoryAware: true },
});

If you skip initSWR, the library works with in-memory cache only -- no persistence, no cross-tab sync.

InitOptions

| Field | Type | Default | Description | | ----------- | ------------------- | ------------------------------------------------- | ------------------------------------------------------------------ | | storage | CacheStorage | -- | Storage backend (IndexedDB, localStorage, etc.) | | sync | SyncConfig | { enabled: true } | Cross-tab sync via BroadcastChannel | | batching | BatchingConfig | { notifyInterval: 0, storageFlushInterval: 50 } | Observer/storage write batching | | hydration | "eager" \| "lazy" | "eager" | eager loads all entries at startup; lazy loads on first access | | gc | GCConfig | { intervalMs: 60000, enabled: true } | Garbage collection config |

SyncConfig

| Field | Type | Default | Description | | ----------------------- | --------- | ------------ | -------------------------------------------------------------------------------------- | | enabled | boolean | true | Enable/disable cross-tab sync | | channelName | string | "qwik-swr" | BroadcastChannel name | | dedup | boolean | false | Deduplicate fetches across tabs | | dedupTimeout | number | 30000 | Timeout for cross-tab fetch dedup in ms. Clears stale entries if a remote tab crashes. | | subscriptionSync | boolean | false | Sync subscription data across tabs (used by initSubscriptionSync) | | subscriptionDedup | boolean | false | Deduplicate subscription connections via leader election | | heartbeatInterval | number | 3000 | Leader heartbeat interval in ms | | failoverTimeout | number | 10000 | Leader failover timeout in ms | | subscriptionWorkerUrl | string | -- | URL for SharedWorker-based subscription sync |

BatchingConfig

| Field | Type | Default | Description | | ---------------------- | -------- | ------- | -------------------------------------------------------------------- | | notifyInterval | number | 0 | Observer notification batch interval in ms. 0 = microtask batching | | storageFlushInterval | number | 50 | Storage write flush interval in ms |

GCConfig

| Field | Type | Default | Description | | ------------- | --------- | --------- | -------------------------------------------------- | | intervalMs | number | 60000 | GC check interval in ms | | enabled | boolean | true | Enable/disable GC | | maxEntries | number | unlimited | Maximum cache entries before eviction | | memoryAware | boolean | false | Scale thresholds based on navigator.deviceMemory |

API Reference

useSWR(key, fetcher$, options?)

The main hook for data fetching.

| Param | Type | Description | | ---------- | ------------------------------------------------------------ | ----------------------------------------------------------------------- | | key | SWRKey \| Signal<SWRKey> | Cache key. Pass a falsy value to disable fetching. Wrap in a Signal for reactive key changes. | | fetcher$ | QRL<(ctx: FetcherCtx) => Data \| Promise<Data>> | QRL-wrapped fetcher function. Receives { rawKey, hashedKey, signal }. | | options? | SWROptions<Data> | Per-hook configuration (see below). |

SWRResponse<Data>

The object returned by useSWR:

| Signal | Type | Description | | -------------- | ------------------------------- | --------------------------------------------------------------- | | data | Signal<Data \| undefined> | Fetched data | | error | Signal<SWRError \| undefined> | Structured error with type, message, retryCount, status | | status | Signal<Status> | "idle" / "loading" / "success" / "error" | | fetchStatus | Signal<FetchStatus> | "idle" / "fetching" / "paused" | | isLoading | Signal<boolean> | true when loading with no data yet | | isSuccess | Signal<boolean> | true when data is available | | isError | Signal<boolean> | true when error occurred and no data | | isValidating | Signal<boolean> | true during any fetch (including background) | | isStale | Signal<boolean> | true when data age exceeds staleTime |

| Method | Description | | ------------------------- | ----------------------------------------------------------------------- | | mutate$(data, options?) | Update cached data. Accepts a value or (current) => newData function. | | revalidate$() | Force a re-fetch from the server. |

Status & FetchStatus State Transitions

status represents data state, while fetchStatus represents network state. They transition independently, allowing precise combinations like "has data + background revalidating".

stateDiagram-v2
    direction LR

    state "status (data state)" as S {
        [*] --> idle
        idle --> loading : fetch starts (no data)
        idle --> success : has fallbackData
        loading --> success : fetch succeeded
        loading --> error : failed (retries exhausted)
        success --> success : revalidation succeeded
        success --> error : revalidation failed + data lost
        error --> loading : revalidate$()
        error --> success : retry succeeded
    }
stateDiagram-v2
    direction LR

    state "fetchStatus (network state)" as F {
        [*] --> idle
        idle --> fetching : fetch starts
        fetching --> idle : completed / error
        idle --> paused : went offline
        fetching --> paused : went offline
        paused --> fetching : back online
    }

SWROptions<Data>

| Option | Type | Default | Description | | ------------------ | ------------------------------------------ | ------------------------ | ---------------------------------------------------------------- | | freshness | FreshnessPreset | "normal" | Preset for staleTime/cacheTime/dedupingInterval | | staleTime | number | (from preset) | Milliseconds before data is considered stale | | cacheTime | number | (from preset) | Milliseconds before cache entry is garbage collected | | dedupingInterval | number | (from preset) | Milliseconds to deduplicate identical requests | | revalidateOn | ("focus" \| "reconnect" \| "interval")[] | ["focus", "reconnect"] | Auto revalidation triggers | | refreshInterval | number | 0 | Polling interval in ms (requires "interval" in revalidateOn) | | retry | boolean \| number | 3 | Max retry count (false to disable) | | retryInterval | number \| (count, error) => number | 1000 | Retry delay in ms (exponential backoff applied) | | timeout | number | 30000 | Fetch timeout in ms | | fallbackData | Data | -- | Initial data (e.g. from SSR) | | keepPreviousData | boolean | false | Keep previous key's data while fetching new key (Signal keys) | | enabled | boolean | true | Set false to disable the hook entirely | | eagerness | "visible" \| "load" \| "idle" | "visible" | Maps to Qwik's useVisibleTask$ strategy | | onSuccess$ | QRL<(data, key) => void> | -- | Callback on successful fetch | | onError$ | QRL<(error, key) => void> | -- | Callback on fetch error |


useMutation(mutationFn$, options?)

Independent mutation hook with optimistic updates and cache invalidation.

| Param | Type | Description | | ------------- | ---------------------------------------------- | ----------------------------------- | | mutationFn$ | QRL<(variables: Variables) => Promise<Data>> | QRL-wrapped mutation function. | | options? | MutationOptions<Data, Variables> | Per-hook configuration (see below). |

MutationResponse<Data, Variables>

The object returned by useMutation:

| Field | Type | Description | | -------------- | ---------------------------------------------- | -------------------------------------------------------- | | mutate$ | QRL<(variables: Variables) => void> | Trigger mutation (fire-and-forget, errors are swallowed) | | mutateAsync$ | QRL<(variables: Variables) => Promise<Data>> | Trigger mutation with Promise return for async handling | | reset$ | QRL<() => void> | Reset all mutation state | | data | Data \| undefined | Result data from successful mutation | | error | SWRError \| undefined | Error from failed mutation | | variables | Variables \| undefined | Last variables passed to the mutation | | isIdle | boolean | true when no mutation has been called yet | | isPending | boolean | true while mutation is in progress | | isSuccess | boolean | true after successful mutation | | isError | boolean | true after failed mutation |

MutationOptions<Data, Variables>

| Option | Type | Default | Description | | ------------------ | --------------------------------- | ------- | -------------------------------------------------- | | optimisticUpdate | OptimisticUpdateConfig | -- | Optimistic update config with key and updater$ | | invalidateKeys | SWRKey[] | -- | Cache keys to revalidate after successful mutation | | onSuccess$ | QRL<(data, variables) => void> | -- | Callback on successful mutation | | onError$ | QRL<(error, variables) => void> | -- | Callback on mutation error |

OptimisticUpdateConfig:

| Field | Type | Description | | ---------- | ----------------------------------------------------------------- | ----------------------------------- | | key | SWRKey | Cache key to update optimistically | | updater$ | QRL<(current: Data \| undefined, variables: Variables) => Data> | Function to produce optimistic data |

Mutation Flow

sequenceDiagram
    participant U as UI
    participant C as Cache
    participant S as Server

    U->>C: mutate$({ title: "New" })
    Note over C: snapshot = current data
    C->>U: optimisticUpdate (instant UI)
    C->>S: POST /api/todos
    alt Success
        S-->>C: 201 Created
        C->>C: invalidateKeys refetch
        C->>U: update with server data
    else Failure
        S-->>C: Error
        C->>C: rollback to snapshot
        C->>U: restore original data
    end

Usage

import { useSWR, useMutation } from "qwik-swr";

const { data: todos } = useSWR("/api/todos", fetchTodos$);

const { mutate$, mutateAsync$, isPending, isError, error } = useMutation(
  $(async (newTodo: { title: string }) => {
    const res = await fetch("/api/todos", {
      method: "POST",
      body: JSON.stringify(newTodo),
    });
    if (!res.ok) throw new Error("Failed");
    return res.json();
  }),
  {
    invalidateKeys: ["/api/todos"],
    optimisticUpdate: {
      key: "/api/todos",
      updater$: $((current, input) => [...(current ?? []), { id: Date.now(), ...input }]),
    },
  },
);

// Fire and forget
await mutate$({ title: "New Todo" });

// Or await the result
const result = await mutateAsync$({ title: "New Todo" });

useSubscription(key, subscriber$, options?)

Real-time data subscription with automatic reconnection (exponential backoff).

Note: Subscription features are in a separate entry point (qwik-swr/subscription) to keep the main bundle small.

| Param | Type | Description | | ------------- | ------------------------------------------------------------ | ------------------------------------------------ | | key | string \| readonly KeyAtom[] \| null \| undefined \| false | Subscription key. Pass a falsy value to disable. | | subscriber$ | QRL<Subscriber<Data, K>> | QRL-wrapped subscriber function (see below). | | options? | SubscriptionOptions<Data> | Per-hook configuration (see below). |

Subscriber<Data, K>

The subscriber function receives the key and callbacks, and must return an { unsubscribe } handle:

type Subscriber<Data, K> = (
  key: K,
  callbacks: {
    onData: (data: Data) => void;
    onError: (error: Error | SWRError) => void;
  },
) => { unsubscribe: () => void } | Promise<{ unsubscribe: () => void }>;

SubscriptionResponse<Data>

The object returned by useSubscription:

| Field | Type | Description | | ---------------- | ----------------------- | -------------------------------------------- | | data | Data \| undefined | Latest data from the subscription | | error | SWRError \| undefined | Error if the connection failed | | status | SubscriptionStatus | "connecting" / "live" / "disconnected" | | isConnecting | boolean | true when status is "connecting" | | isLive | boolean | true when status is "live" | | isDisconnected | boolean | true when status is "disconnected" | | unsubscribe$ | QRL<() => void> | Manually close the subscription | | reconnect$ | QRL<() => void> | Manually reconnect after disconnection |

SubscriptionOptions<Data>

| Option | Type | Default | Description | | ------------------- | ------------------------------------------- | ------- | ---------------------------------------------------- | | maxRetries | number | 10 | Maximum reconnection attempts | | retryInterval | number | 1000 | Base retry delay in ms (exponential backoff applied) | | connectionTimeout | number | 30000 | Timeout for initial connection in ms | | onData$ | QRL<(data: Data) => void> | -- | Callback when data arrives | | onError$ | QRL<(error: SWRError) => void> | -- | Callback when error occurs | | onStatusChange$ | QRL<(status: SubscriptionStatus) => void> | -- | Callback on status change |

Subscription Status Transitions

stateDiagram-v2
    direction LR

    state "SubscriptionStatus" as SS {
        [*] --> connecting : subscribe
        connecting --> live : onData received
        connecting --> disconnected : error / timeout
        live --> disconnected : connection lost
        disconnected --> connecting : auto-retry / reconnect$()
    }

Usage

import { useSubscription } from "qwik-swr/subscription";

const { data, error, status, isLive, unsubscribe$, reconnect$ } = useSubscription(
  "notifications",
  $((key, { onData, onError }) => {
    const ws = new WebSocket(`wss://api.example.com/${key}`);
    ws.onmessage = (e) => onData(JSON.parse(e.data));
    ws.onerror = () => onError(new Error("Connection lost"));
    return { unsubscribe: () => ws.close() };
  }),
  { maxRetries: 10, retryInterval: 1000 },
);

SWRProvider

Wraps a component tree to provide global SWR defaults.

import { SWRProvider } from "qwik-swr";

export default component$(() => (
  <SWRProvider
    config={{
      freshness: "normal",
      revalidateOn: ["focus", "reconnect"],
      retry: 3,
    }}
  >
    <Slot />
  </SWRProvider>
));

Hook-level options take priority over provider config.


cache

Programmatic cache access, usable outside of components.

import { cache } from "qwik-swr";

| Method | Signature | Description | | ------------ | ------------------------------------------ | ------------------------------------------------------------------------------------------------------- | | get | get<Data>(key): CacheEntry<Data> \| null | Retrieve a cache entry | | mutate | mutate<Data>(key, data, options?) | Update entry. data can be a value or (current) => newData. options.revalidate defaults to true. | | revalidate | revalidate(keyOrFilter) | Revalidate by key or predicate (key: HashedKey) => boolean | | delete | delete(key) | Remove entry and abort in-flight request | | clear | clear() | Remove all entries and abort all in-flight requests | | keys | keys(): HashedKey[] | List all cached keys | | prefetch | prefetch<Data>(key, fetcher, options?) | Prefetch data. Returns { promise, abort }. options.force refetches even if cached. | | export | export(): CacheExport | Serializable snapshot of all entries | | import | import(data, options?) | Import entries. options.strategy: "merge" (default) or "overwrite" |

cache.get<User[]>("/api/users"); // CacheEntry<User[]> | null
cache.mutate("/api/users", newData); // Update entry
cache.delete("/api/users"); // Remove entry
cache.clear(); // Remove all entries
cache.keys(); // List all hashed keys

// Prefetch (e.g. on hover)
const { promise, abort } = cache.prefetch(`/api/users/${id}`, async ({ signal }) => {
  const res = await fetch(`/api/users/${id}`, { signal });
  return res.json();
});

// Export/import cache state
const snapshot = cache.export();
cache.import(snapshot, { strategy: "merge" });

Teardown

Clean up all qwik-swr state (useful for tests and app shutdown):

import { teardownSWR } from "qwik-swr";
import { teardownSubscription } from "qwik-swr/subscription";

teardownSWR(); // Resets cache, GC, timers, event listeners
teardownSubscription(); // Resets subscription registry and sync

teardownSWR does not import subscription code, keeping the bundle separation intact.

Guides

Array Keys

Use an array key for compound cache keys. The key is hashed for cache lookup, and ctx.rawKey preserves the original tuple with full type inference.

const { data } = useSWR(
  ["users", page] as const,
  $(async (ctx) => {
    const [resource, pageNum] = ctx.rawKey; // typed as readonly ["users", number]
    const res = await fetch(`/api/${resource}?page=${pageNum}`, { signal: ctx.signal });
    return res.json();
  }),
);

Note: You can use Signal<SWRKey> for reactive keys (e.g. pagination) to automatically re-fetch when the key changes. Alternatively, extract the fetching logic into a child component and use the key prop to force remount. Use keepPreviousData: true for smooth pagination transitions. See examples/basic/src/routes/demo/pagination/ for a working example.

Conditional Fetching

Pass null, undefined, or false as the key to disable fetching. The hook returns status: "idle" with no network request.

const { data } = useSWR(userId ? `/api/users/${userId}` : null, fetcher$);

You can also use a Signal<SWRKey> for reactive conditional fetching:

import { useSignal, useComputed$ } from "@builder.io/qwik";

const token = useSignal<string | null>(null);
const keySignal = useComputed$(() => (token.value ? `/api/me` : null));
const { data } = useSWR(keySignal, fetcher$);

Reactive Keys (Signal)

Wrap the key in a Signal<SWRKey> to automatically re-fetch when the key changes. This is useful for authentication-dependent keys, feature flags, or dynamic filters.

import { useSignal, useComputed$, component$, $ } from "@builder.io/qwik";
import { useSWR } from "qwik-swr";

export default component$(() => {
  const userId = useSignal<number | null>(null);

  // Key is null until user logs in, then becomes "/api/users/123"
  const keySignal = useComputed$(() =>
    userId.value !== null ? `/api/users/${userId.value}` : null,
  );

  const { data, status } = useSWR(
    keySignal,
    $(async (ctx) => {
      const res = await fetch(ctx.rawKey, { signal: ctx.signal });
      return res.json();
    }),
  );

  return <div>{/* ... */}</div>;
});

When the Signal value changes:

  • disabled -> valid: fetch starts
  • valid -> disabled: state resets to idle, resources cleaned up
  • valid A -> valid B: A's lifecycle is torn down, B's fetch starts

Use keepPreviousData: true to retain the old key's data until the new key's data arrives:

const { data } = useSWR(keySignal, fetcher$, { keepPreviousData: true });
// data shows previous key's value while new key is loading

Note: keepPreviousData does NOT preserve data when transitioning to a disabled key (null/undefined/false). Disabled transitions always reset state.

Freshness Presets

Six built-in presets control caching behavior:

| Preset | staleTime | cacheTime | dedupingInterval | Use Case | | ---------- | --------- | --------- | ---------------- | --------------------------------------- | | volatile | 0 | 0 | 2s | Real-time data, always re-fetch | | eager | 0 | 30s | 2s | Fresh data preferred, cache as fallback | | fast | 10s | 60s | 5s | Frequently updated data | | normal | 30s | 5min | 5s | General purpose (default) | | slow | 5min | 1h | 30s | Rarely changing data | | static | Infinity | Infinity | Infinity | Immutable data, fetch once |

useSWR("/api/realtime", fetcher$, { freshness: "volatile" });
useSWR("/api/config", fetcher$, { freshness: "static" });

You can also override individual values:

useSWR("/api/users", fetcher$, {
  freshness: "normal",
  staleTime: 60_000, // override just staleTime
});

Optimistic Updates

Use mutate$ to update the cache immediately, then optionally revalidate in the background.

const { data, mutate$, revalidate$ } = useSWR("/api/todos", fetchTodos$);

// Optimistic add (revalidates by default)
await mutate$((current) => [...(current ?? []), newTodo]);

// Update without revalidation
await mutate$(updatedTodos, { revalidate: false });

// Force re-fetch from server
await revalidate$();

Error Handling & Retry

Errors are structured as SWRError with type, message, retryCount, and status fields.

const { data, error, isError } = useSWR("/api/data", fetcher$, {
  retry: 3,
  retryInterval: 1000, // exponential backoff: 1s, 2s, 4s
  onError$: $((err) => {
    console.log(`${err.type}: ${err.message} (attempt ${err.retryCount})`);
  }),
});

Error types: "network" | "http" | "parse" | "timeout" | "abort" | "business" | "unknown".

SSR with routeLoader$

Combine routeLoader$ (SSR) + useSWR (cache) + useSubscription (realtime) for a seamless three-layer architecture from initial render to live updates.

flowchart LR
    subgraph Server
        RL[routeLoader$]
    end

    subgraph Client
        SWR[useSWR]
        SUB[useSubscription]
        UI[UI]
    end

    RL -- "fallbackData\n(SSR HTML)" --> SWR
    SWR -- "data Signal" --> UI
    SWR -. "stale? revalidate\n(background fetch)" .-> SWR
    SUB -- "onData$ → mutate$\n(cache injection)" --> SWR
    SUB -. "WebSocket / SSE\n(realtime)" .-> SUB

Pre-load data on the server with Qwik City's routeLoader$ and pass it as fallbackData. The page renders immediately with data, and the client revalidates in the background if stale.

import { routeLoader$ } from "@builder.io/qwik-city";
import { useSWR } from "qwik-swr";

export const usePostsLoader = routeLoader$(async () => {
  const res = await fetch("https://api.example.com/posts");
  return res.json();
});

export default component$(() => {
  const loader = usePostsLoader();

  const { data } = useSWR("/api/posts", fetcher$, {
    fallbackData: loader.value,
  });

  // data.value is available on first render -- no loading spinner
  return <PostList posts={data.value} />;
});

Combine with useSubscription for SSR + real-time:

import { useSWR } from "qwik-swr";
import { useSubscription } from "qwik-swr/subscription";

const swr = useSWR("/api/messages", fetcher$, { fallbackData: loader.value });

useSubscription("chat", subscriber$, {
  onData$: $(async (messages) => {
    swr.mutate$(messages, { revalidate: false });
  }),
});

Cross-tab Sync

When initSWR is called with sync enabled (the default), cache mutations propagate to all open tabs via BroadcastChannel. No extra code is needed -- useSWR hooks in other tabs receive the updated data automatically.

await initSWR({
  sync: {
    channelName: "my-app", // isolate from other apps on same origin
    dedup: true, // suppress duplicate fetches across tabs
    dedupTimeout: 30_000, // safety timeout (default: 30s)
  },
});

With dedup: true, if Tab A starts fetching /api/users, Tab B will wait for that result instead of firing a second request. If Tab A crashes or closes before completing the fetch, the dedupTimeout ensures that Tab B resumes fetching after the specified duration (default: 30 seconds) instead of waiting forever.

Subscription Cross-tab Sync

Subscription sync is handled separately via initSubscriptionSync from qwik-swr/subscription. Call it after initSWR:

import { initSWR } from "qwik-swr";
import { initSubscriptionSync } from "qwik-swr/subscription";

await initSWR({
  sync: { channelName: "my-app" },
});

// Enable subscription data sync and connection dedup across tabs
initSubscriptionSync({
  sync: {
    channelName: "my-app",
    subscriptionSync: true,
    subscriptionDedup: true,
  },
});

With subscriptionDedup: true, only one tab holds the real WebSocket/SSE connection per subscription key (leader election). Other tabs receive data via BroadcastChannel or SharedWorker.

Performance Tuning

Notification batching groups multiple observer updates into a single flush. The default notifyInterval: 0 batches per microtask. Set a higher value (e.g. 16 for ~60 fps) to reduce re-renders under heavy write loads.

await initSWR({
  batching: {
    notifyInterval: 16, // batch observer notifications every 16ms
    storageFlushInterval: 100, // coalesce storage writes every 100ms
  },
});

Lazy hydration defers loading cache entries from storage until they are first accessed. This is useful when the cache is large and startup latency matters.

await initSWR({
  storage: createIndexedDBStorage(),
  hydration: "lazy",
});

Memory Management

Use maxEntries to cap the number of cache entries. When the limit is reached, the oldest entries are evicted first (LRU-style by timestamp).

await initSWR({
  gc: {
    maxEntries: 200,
    memoryAware: true, // scale limit based on navigator.deviceMemory
  },
});

With memoryAware: true, the effective maxEntries is scaled by navigator.deviceMemory (when available). On a 4 GB device, the limit stays as configured; on a 2 GB device it is halved.

You can also run GC manually:

import { runGC, stopGC } from "qwik-swr";

runGC({ maxEntries: 100 }); // one-off sweep
stopGC(); // stop the periodic timer

Storage Plugins

The library ships five storage implementations in qwik-swr/storage:

| Factory | Backend | Async | Use Case | | -------------------------- | ------------------- | ----- | -------------------------------------- | | createMemoryStorage() | Map | No | Testing, ephemeral cache | | createLocalStorage() | localStorage | No | Small datasets, simple persistence | | createIndexedDBStorage() | IndexedDB | Yes | Large datasets, structured persistence | | createHybridStorage() | Memory + Persistent | Mixed | Fast reads + durable writes | | createBatchedStorage() | Wraps any storage | Same | Coalesced writes for high-throughput |

import { createIndexedDBStorage, createMemoryStorage, createHybridStorage } from "qwik-swr/storage";

// Hybrid: fast in-memory reads, IndexedDB persistence
const storage = createHybridStorage({
  memory: createMemoryStorage(),
  persistent: createIndexedDBStorage({ dbName: "my-app" }),
});

await initSWR({ storage });

createBatchedStorage is applied automatically when you set batching.storageFlushInterval in initSWR. You only need it directly if you want batching without initSWR.

SWRDevtools

Debug panel for inspecting cache state.

import { SWRDevtools } from "qwik-swr/devtools";

// In your layout (DEV only)
{
  import.meta.env.DEV && <SWRDevtools position="bottom-right" />;
}

Examples

See examples/basic/ for a full Qwik City app demonstrating all features:

  • Basic fetch
  • Array key pagination
  • Conditional fetching
  • Freshness preset comparison
  • Optimistic mutations (useSWR.mutate$)
  • Error handling with retry
  • SSR integration with routeLoader$
  • useMutation -- independent mutation hook with optimistic updates
  • useSubscription -- real-time subscription with auto-reconnect
  • cache.prefetch -- hover-to-prefetch for instant navigation
  • SWRDevtools -- cache debug panel with revalidate/delete/export/import
  • SSR + SWR + Subscription -- three-layer real-time architecture
  • Cross-tab sync -- cache propagation across browser tabs
cd examples/basic
npm install
npm run dev

License

ISC