SwoffSwoff

Adapters

Swoff config and adapters for all React-based frameworks.

React

All React-based frameworks (Vite, Next.js, Remix, TanStack Start) share the same adapter layer — the CLI generates 16 React hooks into swoff/adapters/. The only differences are config values in swoff.config.json.

Astro with React islands: See the dedicated Astro page if you're using Astro (with or without React). The React hooks are still available for client:load React components.

Config per flavor

SettingReact + ViteNext.js (static)RemixTanStack Start
build.swOutput"dist""out""build/client""dist"

| navigation.mode | "spa" | "spa" | "spa" | "spa" | | navigation.fallback | "/index.html" | "/index.html" | "/index.html" | "/index.html" | | navigation.preload | true | true | true | true | | strategy.ignoreQueryParams | [] | ["_rsc"] | ["_data"] | [] |

All other config fields (strategy.default, strategy.patterns, features.*) are the same across flavors.

For detailed setup guides, see React SPA, TanStack Start, and Next.js.

Generated adapters

The CLI generates these hooks into swoff/adapters/. Import them directly:

import { useSwoffFetch } from "./swoff/adapters/useSwoffFetch";
HookConditionPurpose
useSwoffFetchalwaysReactive fetch with caching and auth
useSwoffMutationalwaysMutate data with SW awareness
useSwoffPrefetchalwaysPrefetch resources before navigation
useSwoffResetalwaysWipe all caches, IDB, SW
useSwoffMutationStatealwaysPer-mutation lifecycle tracking
useSwoffQueuemutationQueue.enabledOffline write queue
useSwoffNetworkalwaysOnline/offline state, connection type, retry
useSwoffAuthauth.enabledAuth state, login/logout
useSwoffPrecachealwaysBackground precaching progress
useSwoffPushpushNotificationsPush notification subscription
useSwoffSyncmutationQueue.backgroundSyncBackground sync registration
useSwoffPwapwa.enabledInstall prompt handler
useSwoffAnalyticsalwaysTrack offline fallback events
useSwoffStoragealwaysStorage quota and usage

Hook API

useSwoffFetch<T>(url, options?)

const { data, error, loading, refetch } = useSwoffFetch<Todo[]>("/api/todos");
const { data: posts } = useSwoffFetch<Post[]>("/api/posts", {
  enabled: !!user,
});

Returns { data: T \| null, error: unknown, loading: boolean, refetch: () => void }.

  • Auto-refetches on cache-invalidated events matching the URL
  • Dependent queries: pass enabled: false or a nullable URL to skip until a condition is met
  • Stale data is automatically refreshed in the background by the SW (batched & rate-limited) when using the reactive strategy. On online event, reactive entries with refetchOnReconnect are recovered.
OptionTypeDefaultDescription
select(data: T) => TSelectedidentityTransform the response data. Uses useRef to skip re-renders when the selected value is equal (Object.is) to the previous one.
keepPreviousDatabooleanfalseWhen true, keeps the previous successful data during a background refetch instead of showing a loading state. The returned data is stable — it only updates when the new fetch completes.
placeholderDataT | nullnullInitial data to show while the first fetch is in-flight. Useful for skeleton UI.
onSuccess(data: T) => voidCallback fired when a fetch succeeds. Runs at the hook level (every successful fetch).
onError(err: unknown) => voidCallback fired when a fetch fails. Runs at the hook level (every failed fetch).
enabledbooleantrueWhen false, the fetch is skipped entirely. Useful for dependent queries.

useSwoffMutation<TData>(url, options?)

const {
  mutate,
  isLoading,
  isError,
  isSuccess,
  data,
  error,
  reset,
  mutationId,
} = useSwoffMutation<{ id: number }>("/api/todos", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  auth: true,
  onSuccess: (data) => navigate(`/item/${data.id}`),
  onError: (err) => console.error(err),
  onMutate: () => {
    /* optimistic update */
  },
});
mutate(JSON.stringify({ title: "New" }));

The endpoint and request config (method, headers, auth) are fixed at the hook level. All calls to mutate share the same isLoading, error, data, and isSuccess state.

mutate() returns a MutateResult<T>:

StatusMeaningdataerror
"success"Server responded with 2xxT
"queued"Offline — stored in mutation queue
"error"Server/network errorError
"skipped"Deduped — same mutationKey already in-flight
OptionTypeDescription
urlstringEndpoint URL. Required — fixed at the hook level.
methodstringHTTP method (POST, PUT, PATCH, DELETE, etc.).
headersRecord<string, string>Request headers.
authbooleanAttach auth headers from the auth store.
onSuccess(data: T) => voidHook-level success callback. Runs for every successful mutation.
onError(error: Error) => voidHook-level error callback. Runs for every failed mutation.
onMutate() => voidCalled before the mutation request fires. Use for optimistic updates — mutate your local state here, roll back in onError.
onSettled() => voidRuns after success or error, regardless of outcome.
mutationKeystringDeduplication key — if a mutation with the same key is already in-flight, the new call is skipped.
retrynumber | booleanRetry count on failure (default 0). true = Infinity.

useSwoffPrefetch()

const prefetch = useSwoffPrefetch();
<a onMouseEnter={() => prefetch("/api/todos")}>Todos</a>

Returns a stable prefetch(input, options?) callback. Internally tracks a prefetchList (string array of prefetched URLs) which is accessible for debugging. Call clear() to reset the list.

useSwoffReset()

const { reset, isResetting, error } = useSwoffReset();
FieldTypeDescription
reset(options?) => Promise<void>Calls resetSwoff() with optional { clearStorage, unregisterSW }
isResettingbooleantrue while the reset is in progress
errorunknownLast error from a failed reset attempt, or null

useSwoffMutationState(id)

const mutation = useSwoffMutationState(mutationId);
if (mutation?.status === "error") {
  /* show error */
}

Returns MutationState | null. Subscribes to a specific mutation's state changes. Pass null or empty string to disable.

useSwoffQueue()

const { pending, items, lastSync, isProcessing, retryAll } = useSwoffQueue();
FieldTypeDescription
pendingnumberCount of mutations waiting to sync
itemsMutationQueueItem[]All pending items with status, retry count, URL, method
lastSync{ succeeded: number, failed: number } | nullResult of the last sync attempt
isProcessingbooleantrue while the mutation queue is actively processing items (derived from mutation-sync-complete custom event)
retryAll() => Promise<void>Manually trigger a full replay of all queued mutations

useSwoffNetwork()

const { online, wasOffline, lastChangedAt, effectiveType, downlink } =
  useSwoffNetwork();
FieldTypeDescription
onlinebooleanCurrent online status
wasOfflinebooleantrue if the browser was offline at any point since the hook mounted
lastChangedAtDate | nullTimestamp of the last online/offline transition
effectiveTypestringNetwork effective type ("slow-2g", "2g", "3g", "4g") from navigator.connection
downlinknumberEstimated downlink speed in Mb/s from navigator.connection

useSwoffAuth()

const {
  authenticated,
  auth,
  online,
  isLoading,
  error,
  setAuth,
  clearAuth,
  ensureValid,
} = useSwoffAuth();
FieldTypeDescription
authenticatedbooleanWhether the user is authenticated
authAuthData | nullFull auth data (typed via AuthData interface). Access user via auth.user.
onlinebooleanCurrent online status (from connectivity manager)
isLoadingbooleantrue while checking auth state on mount
errorunknownLast auth error, or null
setAuth(data: AuthData) => Promise<void>Manually set auth data
clearAuth() => Promise<void>Clear auth data (cascades: memory → IDB → caches → SW broadcast)
ensureValid() => Promise<boolean>Check auth validity and refresh if needed. Returns true if valid.

Reacts to sw-auth-state-change events dispatched by clearAuth(), setAuth(), or the SW's AUTH_CLEARED broadcast for cross-tab sync.

useSwoffPrecache()

const { progress } = useSwoffPrecache();

Returns progress (0–100) from the background precache phase. Show the indicator when progress > 0 && progress < 100.

useSwoffPush()

const {
  subscribed,
  subscription,
  permission,
  loading,
  subscribe,
  unsubscribe,
} = useSwoffPush();

Generated when features.pushNotifications is true.

useSwoffSync()

const { supported, registered, lastSync, triggerSync } = useSwoffSync();

Generated when features.mutationQueue.backgroundSync.

useSwoffPwa()

const { isInstallable, promptInstall } = useSwoffPwa();

Generated when features.pwa.enabled.

useSwoffAnalytics(callback?)

const { lastEvent, events, clear } = useSwoffAnalytics();

Tracks offline navigation fallback events. Optionally pass a callback that fires on each event. clear() resets the event list.

useSwoffStorage()

const {
  usage,
  quota,
  percentUsed,
  formattedUsage,
  formattedQuota,
  loading,
  error,
} = useSwoffStorage();
FieldTypeDescription
usagenumberBytes used (from navigator.storage.estimate())
quotanumberTotal quota in bytes
percentUsednumberPercentage of quota used (0–100)
formattedUsagestringHuman-readable usage (e.g. "1.5 MB")
formattedQuotastringHuman-readable quota (e.g. "100 MB")
loadingbooleantrue while the estimate is being fetched
errorstring | nullError message if the estimate failed

By default, re-checks on every visibilitychange to "visible" (pass false to disable auto-refresh).

Entry point

For all React flavors, import initServiceWorker in your root component:

import { useEffect } from "react";
import { initServiceWorker } from "./swoff/client-injector";

function App() {
  useEffect(() => {
    initServiceWorker();
  }, []);
  return <YourApp />;
}

See the Next.js guide for App Router and Pages Router setup. Remix: call in app/root.tsx client entry.

Build script

Run the SW generator after your build:

node swoff/sw/generator.mjs

Or add it to package.json:

{
  "scripts": {
    "build": "your-framework-build && node swoff/sw/generator.mjs"
  }
}

Next.js static export output goes to out/ — the SW generator reads build.swOutput from config and writes there automatically.

On this page