axios-offline-queue
v1.0.5
Published
[](https://www.npmjs.com/package/axios-offline-queue) [](https://www.npmjs.com/package/axios-offline-queue) [![License: MIT]
Maintainers
Readme
axios-offline-queue
Powerful utilities for offline request queueing, seamless Axios integration, and robust connectivity detection for React and JavaScript applications.
📚 Table of Contents
- 🏆 Features
- 📝 Changelog
- 🔧 Installation
- 🚀 Quick Start
- 📦 API & Exports
- 🧩 Basic Offline Queue Example
- 📚 React Full Example
- 🔍 Offline GET Hook (
useOfflineGet) - 🧱 Service Usage (
offlineGetHandler) - 🔔 Event System
- 📌 Optimistic UI & Pending Mutations
- 🤝 Contributing & Issues
- 📄 License
- 👤 Author
🏆 Features
- Transparent Axios interceptors for offline queueing: Requests are automatically queued when offline and retried when connectivity returns.
- Per-request fine-grained control: You can prevent a specific request from being queued by setting the
X-Queue-Offline: falseheader. If this header is present (with any value considered falsy, like'false'orfalse), such requests will not be added to the offline queue. This is especially useful for requests that must only be executed online and should not be replayed later.
Example:axios.post("/api/data", body, { headers: { "X-Queue-Offline": "false" } }) - Auto-connectivity detection: Monitors both network status and Axios server reachability.
- LocalStorage-based persistent queue: Survives page reloads and crashes.
- Hooks for React: Simple state and cache management for your React UI.
- Manual queue management utilities: Query queue size, clear queue, and handle events.
- Optimistic UI helpers (no visual layer): Optional headers
X-Offline-Client-Entity-IdandX-Offline-Metadataare stripped before persist/retry;projectPendingView,reconcilePendingWithResponse, andofflineQueue.getPendingRequests()support merging pending mutations with your cached GET data. - Queue lifecycle events:
pending-enqueuedandpending-removed(with reason) complementrequest-success/request-failedfor UI state. - Fully typed and documented API.
- No runtime dependencies except
axios. React is only required if using hooks.
📝 Changelog
Latest (1.0.5)
- Optimistic UI & pending mutations (logic-only):
- Optional request headers
X-Offline-Client-Entity-IdandX-Offline-Metadata(JSON string) are read when enqueueing, stored onQueuedRequest, and never sent to the API on replay. - New helpers:
generateClientEntityId,getClientEntityId,getQueuedRequestBody,projectPendingView,reconcilePendingWithResponse,extractOfflinePresentationFromConfig,stripOfflinePresentationHeaders, plus exported header name constants. offlineQueue.addRequest(config, options?)acceptsAddRequestOptions(clientEntityId,metadata); explicit options override header-derived values. Presentation headers are also applied when usingofflineHook(sameaddRequestpath).- New queue methods:
getPendingRequests(). - New events:
pending-enqueued,pending-removed(payload includesreason:"synced"|"failed"|"cleared"). queue-processedpayload:{ count, requests }— only requests successfully synced (HTTP success and removed from storage) in that processing run; not items still pending retry or left due to network errors.
- Optional request headers
Earlier
- Header-based exclusion: You can now exclude any request from the offline queue by adding the header
X-Queue-Offline: false(either as a string or boolean), allowing fine-grained control over which requests will be automatically retried when you're back online.
Quick example:
With this, that request will never be queued if you're offline.axios.post("/only-online", body, { headers: { "X-Queue-Offline": "false" } }) - **Improved types in
useOfflineGet**: TheuseOfflineGethook now returns not only thedata, but also the complete response (response), aligning the types with the standard Axios pattern (AxiosResponse<TData>). This provides you full access to status, headers, etc., along with the main data.
🔧 Installation
npm install axios-offline-queue
# or
yarn add axios-offline-queue🚀 Quick Start
Axios Integration
Automatically queue failed requests and retry them when back online:
import axios from "axios";
import { offlineHook, initializeOfflineSupport } from "axios-offline-queue";
// Apply offline interceptors to your Axios instance
const api = axios.create({ baseURL: "/api" });
const [onSuccess, onError] = offlineHook({useLogs:true}); // You also can use {}
api.interceptors.response.use(onSuccess, onError);
// Alternatively, you can use destructuring:
/*
* api.interceptors.response.use(...offlineHook());
*/
// Initialize global offline support (call last, with your AxiosInstance)
initializeOfflineSupport(api);Skipping offline queue for specific requests
If you want a certain request to never be queued (for example, login or payment), set the special header X-Queue-Offline: false in that Axios request. This instructs the interceptor not to queue that specific request when offline, letting it fail immediately as if no queue exists.
axios.post("/critical-only-online", data, {
headers: { "X-Queue-Offline": "false" },
});You may also set this header as a boolean value (false) when using some clients.
The header is detected per request; the value is interpreted as falsy for "false", false, or 0.
📦 API & Exports
All exports are fully typed and documented.
| Export | Description |
| -------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| offlineHook | Axios response interceptors for queuing requests when offline. (X-Queue-Offline: false prevents queueing per-request) |
| initializeOfflineSupport | Sets up global listeners for connectivity changes. |
| getOfflineQueueSize | Returns the number of queued requests. |
| clearOfflineQueue | Clears all requests from the offline queue. |
| offlineQueue | Singleton queue instance for advanced/manual manipulation. Includes event system. |
| getConnectivityStatus | Returns { online, serverReachable } representing current network/server reachability. |
| onQueueEvent / offQueueEvent | Low-level API for subscribing/unsubscribing to global queue events (usually not required if you use offlineQueue.on). |
| generateClientEntityId, getClientEntityId, getQueuedRequestBody | Helpers for stable client-side IDs and reading queued bodies. |
| projectPendingView / reconcilePendingWithResponse | Map a QueuedRequest to your list row shape; merge server response.data after sync. |
| extractOfflinePresentationFromConfig / stripOfflinePresentationHeaders | Advanced: strip/read presentation headers from an AxiosRequestConfig. |
| OFFLINE_HEADER_CLIENT_ENTITY_ID / OFFLINE_HEADER_METADATA | Constants for the optional presentation headers (documented below). |
| Types QueuedRequest, AddRequestOptions, PendingEnqueuedPayload, PendingRemovedPayload | Strong typing for queue items and optimistic flows. |
🧩 Basic Offline Queue Example
import { offlineHook, getOfflineQueueSize, clearOfflineQueue } from "axios-offline-queue";
import axios from "axios";
// Setup interceptor
const [respOk, respError] = offlineHook({});
axios.interceptors.response.use(respOk, respError);
// Query queue size
const enCola = getOfflineQueueSize();
// Clear the queue
clearOfflineQueue();
// Example: Making a request that will NOT be queued if offline
axios.post("/not-queued", data, { headers: { "X-Queue-Offline": "false" }});📚 React Full Example
import {
offlineHook,
initializeOfflineSupport,
getOfflineQueueSize,
offlineQueue,
getConnectivityStatus,
useOfflineGet,
} from "axios-offline-queue";
import axios from "axios";
import { useEffect, useState } from "react";
const api = axios.create({ baseURL: "/api" });
const [onFulfilled, onRejected] = offlineHook({ useLogs: true });
api.interceptors.response.use(onFulfilled, onRejected);
initializeOfflineSupport(api);
function ColaOfflineInfo() {
const [size, setSize] = useState(0);
const [{ online, serverReachable }, setStatus] = useState(
getConnectivityStatus(),
);
useEffect(() => {
setSize(getOfflineQueueSize());
const interval = setInterval(() => setSize(getOfflineQueueSize()), 2000);
const unsubscribeQueue = offlineQueue.on("queue-processed", ({ count }) => {
// Finished one cycle; count = successfully synced requests in this run
setSize(getOfflineQueueSize());
});
const unsubscribeConnectionLost = offlineQueue.on(
"connection-lost",
() => {
setStatus(getConnectivityStatus());
},
);
const unsubscribeConnectionRestored = offlineQueue.on(
"connection-restored",
() => {
setStatus(getConnectivityStatus());
},
);
return () => {
clearInterval(interval);
unsubscribeQueue();
unsubscribeConnectionLost();
unsubscribeConnectionRestored();
};
}, []);
return (
<div>
<div>Pending offline requests: {size}</div>
<div>Internet connection: {online ? "Yes" : "No"}</div>
<div>Server reachable: {serverReachable ? "Yes" : "No"}</div>
</div>
);
}
export default ColaOfflineInfo;🔍 Offline GET Hook (useOfflineGet)
In addition to offline queueing for mutation requests, this package provides a specialized hook for GET requests that do not get queued, but can serve the last successful result from cache when the request fails due to connectivity.
Concept
- GETs are not queued.
- If a GET fails due to offline or network error:
- The hook tries to return the last known good result (stored by default in
localStorage).
- The hook tries to return the last known good result (stored by default in
- Fully customizable:
- Define your
fetcher(how your GET is performed) - You can provide a custom
storageif you don't want to uselocalStorage.
- Define your
API
The return type of the hook has been improved to closely match Axios's pattern. Now, instead of returning just data, you also get a response and the typing matches the AxiosResponse<TData> (see below).
import type { AxiosResponse } from "axios";
// options
type UseOfflineGetOptions<TData, TResponse = AxiosResponse<TData>> = {
key: string;
fetcher: () => Promise<TResponse>;
initialData?: TData;
storage?: {
get: (key: string) => TData | null;
set: (key: string, value: TData) => void;
remove?: (key: string) => void;
};
/**
* Custom serialization for the default (localStorage) storage.
* Ideal for encryption or data transformation.
* If `storage` is provided, these functions are ignored.
*/
serialize?: (value: TData) => string;
deserialize?: (raw: string) => TData;
enabled?: boolean; // default: true
};Returns:
type UseOfflineGetState<TData, TResponse = AxiosResponse<TData>> = {
data: TData | null;
response?: TResponse | null; // RESPONSE: Now aligned with Axios pattern
isLoading: boolean;
error: unknown;
isFromCache: boolean;
hasCache: boolean;
refetch: () => Promise<void>;
};Simple Axios Example
import { useOfflineGet } from "axios-offline-queue";
import api from "./api"; // your axios instance with offlineHook + initializeOfflineSupport
type User = { id: string; name: string };
function UsersList() {
const { data, response, isLoading, error, isFromCache } = useOfflineGet<User[]>({
key: "users-list",
fetcher: () => api.get<User[]>("/users"),
});
if (isLoading && !data) return <div>Loading users...</div>;
if (error && !data) return <div>Error loading users</div>;
return (
<div>
{isFromCache && <small>Showing cached data</small>}
<ul>
{data?.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
{response && <div>Raw status: {response.status}</div>}
</div>
);
}Example with Custom Storage
const myStorage = {
get: (key: string) => myInMemoryCache[key] ?? null,
set: (key: string, value: any) => {
myInMemoryCache[key] = value;
},
remove: (key: string) => {
delete myInMemoryCache[key];
},
};
const state = useOfflineGet({
key: "products",
fetcher: () => api.get("/products"),
storage: myStorage,
});This lets you decide, per GET, how to manage the last successful value (in localStorage, RAM, IndexedDB, etc.) and automatically retrieves it when connectivity is lost, all without touching the global queue logic.
Example: Custom Encryption using localStorage
import { useOfflineGet } from "axios-offline-queue";
import api from "./api";
import { encrypt, decrypt } from "./crypto"; // defined in your project
type SecretData = { /* ... */ };
const serializeSecret = (value: SecretData) =>
encrypt(JSON.stringify(value)); // returns string
const deserializeSecret = (raw: string): SecretData =>
JSON.parse(decrypt(raw));
function SecretComponent() {
const state = useOfflineGet<SecretData>({
key: "secret-key",
fetcher: () => api.get<SecretData>("/secret"),
serialize: serializeSecret,
deserialize: deserializeSecret,
});
// ...
}⚠️ Security: This package does not include out-of-the-box encryption! For sensitive data, you should use
serialize/deserializeor a customstoragewith encryption. Always review your app's security best practices.
🧱 Service Usage (offlineGetHandler)
Prefer to use the same offline cache logic in plain TypeScript services (no hooks)? Use offlineGetHandler inside functions like getUsers, getProducts, etc.
Basic API
import type { AxiosResponse } from "axios";
type OfflineGetHandlerOptions<TData, TResponse = AxiosResponse<TData>> = {
key: string;
fetcher: Promise<TResponse> | (() => Promise<TResponse>);
initialData?: TData;
storage?: {
get: (key: string) => TData | null;
set: (key: string, value: TData) => void;
remove?: (key: string) => void;
};
serialize?: (value: TData) => string;
deserialize?: (raw: string) => TData;
enabled?: boolean;
};
type OfflineGetHandlerResult<TData, TResponse = AxiosResponse<TData>> = {
data: TData | null;
response?: TResponse | null;
isLoading: boolean;
error: unknown;
isFromCache: boolean;
hasCache: boolean;
};Example in services.ts
import { offlineGetHandler } from "axios-offline-queue";
import api from "./api";
type User = { id: string; name: string };
export const getUsers = async (): Promise<User[] | null> => {
try {
const { data, response } = await offlineGetHandler<User[]>({
key: "users",
// You can pass a promise...
fetcher: api.get<User[]>("/users"),
// ...or a function returning a promise:
// fetcher: () => api.get<User[]>("/users"),
});
return data;
} catch (error) {
console.error("Error fetching users", error);
throw error;
}
};Behavior Notes
- If offline,
offlineGetHandler:- Does not attempt the call.
- Returns the last cached value (if available) or
initialData.
- On network/offline error:
- Tries to return cached value.
- If not available, throws so your service can handle it.
- Business (4xx/5xx) errors with no cache:
- Error is propagated directly.
You can use all the same storage, serialize, and deserialize customizations as in the hook—use IndexedDB, encryption, or any store you want.
🔔 Event System
The offlineQueue singleton exposes a robust event system to react to queue events or connectivity changes.
API
- offlineQueue.on(event, handler): Subscribe a listener; returns an unsubscribe function.
- offlineQueue.off(event, handler): Unsubscribe a listener.
Available event types:
- "queue-processed"
- Emitted when
processQueue()finishes one complete cycle over the snapshot that existed at the start of the run. - Payload:
Only requests successfully sent (HTTP success) and removed from storage in that run are included. Items that stay in the queue (network error, 5xx retry, etc.) are not listed here; use per-request events for those.{ count: number; requests: QueuedRequest[]; }
- Emitted when
- "pending-enqueued"
- Emitted when a request is persisted to the offline queue.
- Payload:
{ request: QueuedRequest }
- "pending-removed"
- Emitted when a queued request leaves storage.
- Payload:
{ request: QueuedRequest; reason: "synced" | "failed" | "cleared" }"synced": successful HTTP response, removed from queue."failed": permanent failure (e.g. 4xx, max retries)."cleared":clearOfflineQueue()removed it.
- "request-success"
- Fired when a request is retried and succeeds, and is then removed from the queue.
- Payload:
{ request: QueuedRequest; success: true; response: AxiosResponse; }
- "request-failed"
- Fired when a request is removed from the queue due to a permanent failure (e.g. 4xx error, or exceeded
MAX_RETRY_COUNT). - Payload:
{ request: QueuedRequest; success: false; status: number | null; error: AxiosError | { code?: string; message?: string }; }
- Fired when a request is removed from the queue due to a permanent failure (e.g. 4xx error, or exceeded
- "request-processed"
- Fired for both "request-success" and "request-failed"; payload is a union.
- "connection-lost" / "connection-restored"
- Emitted when the network or server connectivity changes.
{ kind: "network" | "server"; online: boolean; serverReachable: boolean; error?: AxiosError | { code?: string; message?: string }; }
- Emitted when the network or server connectivity changes.
Example: Subscribing to Queue Events
import { offlineQueue } from "axios-offline-queue";
// React when the queue finishes a processing cycle (successful syncs only in payload)
offlineQueue.on("queue-processed", ({ count, requests }) => {
console.log(`Offline queue finished: ${count} request(s) synced.`, requests);
});
// When an item is added to / removed from persisted queue (for optimistic UI)
offlineQueue.on("pending-enqueued", ({ request }) => {
console.log("Queued:", request.id, request.clientEntityId);
});
offlineQueue.on("pending-removed", ({ request, reason }) => {
console.log("Removed from queue:", request.id, reason);
});
// Detect when a request is retried successfully
offlineQueue.on("request-success", ({ request, response }) => {
console.log("Request successfully retried:", request.id, response.status);
});
// Handle permanent failures (4xx, max retries)
offlineQueue.on("request-failed", ({ request, status }) => {
console.warn("Request removed from queue due to permanent failure:", request.id, status);
});
// Connectivity changes
offlineQueue.on("connection-lost", ({ kind }) => {
if (kind === "network") {
console.log("Internet connection lost");
} else {
console.log("Server unreachable, but internet is up");
}
});
offlineQueue.on("connection-restored", ({ kind }) => {
if (kind === "network") {
console.log("Internet connection restored");
} else {
console.log("Server is reachable again");
}
});📌 Optimistic UI & Pending Mutations
This package does not render UI. It provides headers, queue fields (clientEntityId, metadata), events, and pure helpers so your app can show pending rows (e.g. “saved offline”) and merge server responses when sync completes.
Presentation headers (stripped before persist / replay)
| Header | Purpose |
| ------ | ------- |
| X-Offline-Client-Entity-Id | Stable client-generated id (UUID recommended) for list keys until the server returns a real id. |
| X-Offline-Metadata | JSON string with app-specific hints (entity type, ids for delete targets, etc.). |
These headers are removed from the stored AxiosRequestConfig and are not sent to your API. You can also pass AddRequestOptions as the second argument to offlineQueue.addRequest(config, options); it merges with header-derived options (explicit options win).
Core helpers
import {
generateClientEntityId,
projectPendingView,
reconcilePendingWithResponse,
OFFLINE_HEADER_CLIENT_ENTITY_ID,
OFFLINE_HEADER_METADATA,
offlineQueue,
onQueueEvent,
type QueuedRequest,
} from "axios-offline-queue";offlineQueue.getPendingRequests()— snapshot of persisted queue (for building optimistic rows on mount).projectPendingView(request, projector)— mapbody+metadata+clientEntityIdto your list row type.reconcilePendingWithResponse(pendingView, response.data, merge)— replace temp ids / shapes afterrequest-success.- Listen to
pending-enqueued,pending-removed, andrequest-successto keep UI in sync.
Example: Create user (POST)
Use a metadata flag (e.g. entity: "user") so list screens can filter which queued items belong to which view.
import axios from "axios";
import {
generateClientEntityId,
OFFLINE_HEADER_CLIENT_ENTITY_ID,
OFFLINE_HEADER_METADATA,
projectPendingView,
reconcilePendingWithResponse,
} from "axios-offline-queue";
type CreateUserBody = {
nombre: string;
email: string;
password: string;
rol: number;
};
export async function createUser(api: ReturnType<typeof axios.create>, body: CreateUserBody) {
const clientEntityId = generateClientEntityId();
await api.post("/users", body, {
headers: {
[OFFLINE_HEADER_CLIENT_ENTITY_ID]: clientEntityId,
[OFFLINE_HEADER_METADATA]: JSON.stringify({ entity: "user" }),
},
});
}
// Build a “pending” row for your table (no password in UI)
function userRowFromQueue(request: QueuedRequest) {
return projectPendingView(request, ({ body, clientEntityId, metadata }) => {
const m = metadata as { entity?: string } | undefined;
if (m?.entity !== "user") throw new Error("not user");
const b = body as CreateUserBody;
return {
id: clientEntityId,
nombre: b.nombre,
email: b.email,
pending: true,
};
});
}
// After sync: merge server shape into your row type
function mergeUserAfterSync(
pendingRow: { id: string; nombre: string; email: string; pending: boolean },
responseData: unknown,
) {
return reconcilePendingWithResponse(pendingRow, responseData, (pending, server) => {
const s = server as { id: string; nombre: string; email: string };
return {
...pending,
id: s.id,
nombre: s.nombre,
email: s.email,
pending: false,
};
});
}Example: Create role (POST)
await api.post(
"/roles",
{ nombre: "Editor", permisos: [1, 2] },
{
headers: {
[OFFLINE_HEADER_CLIENT_ENTITY_ID]: generateClientEntityId(),
[OFFLINE_HEADER_METADATA]: JSON.stringify({ entity: "role" }),
},
},
);Example: Create blog post (POST)
await api.post(
"/posts",
{ titulo: "Draft", contenido: "..." },
{
headers: {
[OFFLINE_HEADER_CLIENT_ENTITY_ID]: generateClientEntityId(),
[OFFLINE_HEADER_METADATA]: JSON.stringify({ entity: "post" }),
},
},
);Example: Delete another post (DELETE)
Offline deletes rarely have a JSON body; put identifiers in metadata so the UI can mark the correct row (e.g. strikethrough or “deletion pending”).
await api.delete(`/posts/${serverPostId}`, {
headers: {
[OFFLINE_HEADER_CLIENT_ENTITY_ID]: generateClientEntityId(),
[OFFLINE_HEADER_METADATA]: JSON.stringify({
entity: "post-delete",
targetPostId: serverPostId,
titulo: "Title for placeholder row",
}),
},
});In your posts list, if metadata.entity === "post-delete" and metadata.targetPostId matches a row, show an offline badge or hide the row according to your UX.
React: subscribe and refresh optimistic rows
import { useEffect, useState, useCallback } from "react";
import { offlineQueue, onQueueEvent } from "axios-offline-queue";
function usePendingUserRows() {
const [rows, setRows] = useState<ReturnType<typeof userRowFromQueue>[]>([]);
const refresh = useCallback(() => {
const pending = offlineQueue.getPendingRequests();
const out: ReturnType<typeof userRowFromQueue>[] = [];
for (const req of pending) {
try {
const meta = req.metadata as { entity?: string } | undefined;
if (meta?.entity === "user") out.push(userRowFromQueue(req));
} catch {
/* skip */
}
}
setRows(out);
}, []);
useEffect(() => {
refresh();
const u1 = onQueueEvent("pending-enqueued", refresh);
const u2 = onQueueEvent("pending-removed", refresh);
const u3 = onQueueEvent("request-success", refresh);
return () => {
u1();
u2();
u3();
};
}, [refresh]);
return rows;
}Merge these rows with data from useOfflineGet (or your cache) for a single list: dedupe by clientEntityId or server id after request-success.
Note: Sent payloads and server/list shapes often differ (e.g. numeric
rolvs string, omittedpasswordin lists). Projection and reconciliation are your domain; the package only supplies ids, metadata, queue access, and events.
🤝 Contributing & Issues
Have suggestions, feature requests, or found a bug?
Feel free to open an issue or submit a pull request!
- Please provide clear reproduction steps or code snippets for bugs.
- Contributions in English are preferred; Spanish also welcome for community topics.
📄 License
MIT
👤 Author
Developed and maintained by Antonio Benavides
