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

axios-offline-queue

v1.0.5

Published

[![npm version](https://img.shields.io/npm/v/axios-offline-queue)](https://www.npmjs.com/package/axios-offline-queue) [![npm downloads](https://img.shields.io/npm/dm/axios-offline-queue)](https://www.npmjs.com/package/axios-offline-queue) [![License: MIT]

Readme

axios-offline-queue

npm version npm downloads License: MIT

Powerful utilities for offline request queueing, seamless Axios integration, and robust connectivity detection for React and JavaScript applications.

📚 Table of Contents


🏆 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: false header. If this header is present (with any value considered falsy, like 'false' or false), 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-Id and X-Offline-Metadata are stripped before persist/retry; projectPendingView, reconcilePendingWithResponse, and offlineQueue.getPendingRequests() support merging pending mutations with your cached GET data.
  • Queue lifecycle events: pending-enqueued and pending-removed (with reason) complement request-success / request-failed for 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-Id and X-Offline-Metadata (JSON string) are read when enqueueing, stored on QueuedRequest, 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?) accepts AddRequestOptions (clientEntityId, metadata); explicit options override header-derived values. Presentation headers are also applied when using offlineHook (same addRequest path).
    • New queue methods: getPendingRequests().
    • New events: pending-enqueued, pending-removed (payload includes reason: "synced" | "failed" | "cleared").
    • queue-processed payload: { 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.

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:
    axios.post("/only-online", body, { headers: { "X-Queue-Offline": "false" } })
    With this, that request will never be queued if you're offline.
  • **Improved types in useOfflineGet**: The useOfflineGet hook now returns not only the data, 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).
  • Fully customizable:
    • Define your fetcher (how your GET is performed)
    • You can provide a custom storage if you don't want to use localStorage.

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/deserialize or a custom storage with 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:
      {
        count: number;
        requests: QueuedRequest[];
      }
      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.
  • "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 };
      }
  • "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 };
      }

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) — map body + metadata + clientEntityId to your list row type.
  • reconcilePendingWithResponse(pendingView, response.data, merge) — replace temp ids / shapes after request-success.
  • Listen to pending-enqueued, pending-removed, and request-success to 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 rol vs string, omitted password in 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