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:loadReact components.
Config per flavor
| Setting | React + Vite | Next.js (static) | Remix | TanStack 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";| Hook | Condition | Purpose |
|---|---|---|
useSwoffFetch | always | Reactive fetch with caching and auth |
useSwoffMutation | always | Mutate data with SW awareness |
useSwoffPrefetch | always | Prefetch resources before navigation |
useSwoffReset | always | Wipe all caches, IDB, SW |
useSwoffMutationState | always | Per-mutation lifecycle tracking |
useSwoffQueue | mutationQueue.enabled | Offline write queue |
useSwoffNetwork | always | Online/offline state, connection type, retry |
useSwoffAuth | auth.enabled | Auth state, login/logout |
useSwoffPrecache | always | Background precaching progress |
useSwoffPush | pushNotifications | Push notification subscription |
useSwoffSync | mutationQueue.backgroundSync | Background sync registration |
useSwoffPwa | pwa.enabled | Install prompt handler |
useSwoffAnalytics | always | Track offline fallback events |
useSwoffStorage | always | Storage 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-invalidatedevents matching the URL - Dependent queries: pass
enabled: falseor 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
reactivestrategy. Ononlineevent, reactive entries withrefetchOnReconnectare recovered.
| Option | Type | Default | Description |
|---|---|---|---|
select | (data: T) => TSelected | identity | Transform the response data. Uses useRef to skip re-renders when the selected value is equal (Object.is) to the previous one. |
keepPreviousData | boolean | false | When 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. |
placeholderData | T | null | null | Initial data to show while the first fetch is in-flight. Useful for skeleton UI. |
onSuccess | (data: T) => void | — | Callback fired when a fetch succeeds. Runs at the hook level (every successful fetch). |
onError | (err: unknown) => void | — | Callback fired when a fetch fails. Runs at the hook level (every failed fetch). |
enabled | boolean | true | When 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>:
| Status | Meaning | data | error |
|---|---|---|---|
"success" | Server responded with 2xx | T | — |
"queued" | Offline — stored in mutation queue | — | — |
"error" | Server/network error | — | Error |
"skipped" | Deduped — same mutationKey already in-flight | — | — |
| Option | Type | Description |
|---|---|---|
url | string | Endpoint URL. Required — fixed at the hook level. |
method | string | HTTP method (POST, PUT, PATCH, DELETE, etc.). |
headers | Record<string, string> | Request headers. |
auth | boolean | Attach auth headers from the auth store. |
onSuccess | (data: T) => void | Hook-level success callback. Runs for every successful mutation. |
onError | (error: Error) => void | Hook-level error callback. Runs for every failed mutation. |
onMutate | () => void | Called before the mutation request fires. Use for optimistic updates — mutate your local state here, roll back in onError. |
onSettled | () => void | Runs after success or error, regardless of outcome. |
mutationKey | string | Deduplication key — if a mutation with the same key is already in-flight, the new call is skipped. |
retry | number | boolean | Retry 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();| Field | Type | Description |
|---|---|---|
reset | (options?) => Promise<void> | Calls resetSwoff() with optional { clearStorage, unregisterSW } |
isResetting | boolean | true while the reset is in progress |
error | unknown | Last 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();| Field | Type | Description |
|---|---|---|
pending | number | Count of mutations waiting to sync |
items | MutationQueueItem[] | All pending items with status, retry count, URL, method |
lastSync | { succeeded: number, failed: number } | null | Result of the last sync attempt |
isProcessing | boolean | true 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();| Field | Type | Description |
|---|---|---|
online | boolean | Current online status |
wasOffline | boolean | true if the browser was offline at any point since the hook mounted |
lastChangedAt | Date | null | Timestamp of the last online/offline transition |
effectiveType | string | Network effective type ("slow-2g", "2g", "3g", "4g") from navigator.connection |
downlink | number | Estimated downlink speed in Mb/s from navigator.connection |
useSwoffAuth()
const {
authenticated,
auth,
online,
isLoading,
error,
setAuth,
clearAuth,
ensureValid,
} = useSwoffAuth();| Field | Type | Description |
|---|---|---|
authenticated | boolean | Whether the user is authenticated |
auth | AuthData | null | Full auth data (typed via AuthData interface). Access user via auth.user. |
online | boolean | Current online status (from connectivity manager) |
isLoading | boolean | true while checking auth state on mount |
error | unknown | Last 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();| Field | Type | Description |
|---|---|---|
usage | number | Bytes used (from navigator.storage.estimate()) |
quota | number | Total quota in bytes |
percentUsed | number | Percentage of quota used (0–100) |
formattedUsage | string | Human-readable usage (e.g. "1.5 MB") |
formattedQuota | string | Human-readable quota (e.g. "100 MB") |
loading | boolean | true while the estimate is being fetched |
error | string | null | Error 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.mjsOr 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.