axios-cancel-groups
v1.0.1
Published
Grouped request cancellation for Axios with React hooks and Express middleware - Cancel multiple requests by group key, auto-cancel on route changes, and handle timeouts
Maintainers
Readme
axios-cancel-groups
Grouped request cancellation for Axios with React hooks and Express middleware. Cancel multiple requests by group key, auto-cancel on route changes, and handle timeouts elegantly.
Features
- 🎯 Grouped Request Cancellation: Bulk cancel Axios requests by group key
- ⚛️ React Hooks: Context Provider and hooks for automatic request management
- 🔄 Auto-Cancel on Route Change: Automatically cancel requests when navigating
- ⏱️ Express Timeout Middleware: Hard timeout limits with automatic cleanup
- 🔌 Client Disconnect Handling: Auto-cleanup when clients disconnect
- 📦 TypeScript Support: Full type definitions included
- 🌲 Tree-shakeable: ESM and CJS exports
- ✅ Well Tested: 91 tests covering all features
Installation
npm install axios-cancel-groupsPeer Dependencies
npm install axios # Required for Axios features
npm install express # Required for Express middleware
npm install react # Required for React hooksUsage
React - Recommended
Wrap your app with the Context Provider for automatic request management:
// App.tsx - Wrap with Provider
import { RequestCancelProvider } from "axios-cancel-groups";
function App() {
return (
<RequestCancelProvider axiosConfig={{ baseURL: "/api" }}>
<YourApp />
</RequestCancelProvider>
);
}Available Hooks
1. Basic Usage - useAxios and group
import { useAxios, group } from "axios-cancel-groups";
function UserList() {
const [users, setUsers] = useState([]);
const api = useAxios();
useEffect(() => {
// Group requests together
api.get("/users", group("users-page"))
.then(res => setUsers(res.data))
.catch(err => {
if (err.code !== 'ERR_CANCELED') {
console.error(err);
}
});
}, []);
return <div>...</div>;
}2. Auto-Cancellation - useGroupCancellation
import { useAxios, group, useGroupCancellation } from "axios-cancel-groups";
function ProductDetails({ productId }) {
const [product, setProduct] = useState(null);
const api = useAxios();
// Auto-cancel when productId changes or component unmounts
useGroupCancellation(`product-${productId}`, [productId]);
useEffect(() => {
api.get(`/products/${productId}`, group(`product-${productId}`))
.then(res => setProduct(res.data))
.catch(err => console.error(err));
}, [productId]);
return <div>...</div>;
}3. Route Change Cancellation - useRouteCancellation
import { useLocation } from "react-router-dom";
import { useAxios, useRouteCancellation } from "axios-cancel-groups";
function UsersPage() {
const location = useLocation();
const api = useAxios();
const [users, setUsers] = useState([]);
// Auto-cancel when route changes
const routeGroup = useRouteCancellation(location.pathname);
useEffect(() => {
api.get("/users", { group: routeGroup } as any)
.then(res => setUsers(res.data));
}, []);
return <div>...</div>;
}4. Manual Control - useRequestManager
import { useRequestManager } from "axios-cancel-groups";
function Dashboard() {
const { api, requestManager } = useRequestManager();
const [stats, setStats] = useState(null);
const loadDashboard = () => {
api.get("/stats", { group: "dashboard" } as any)
.then(res => setStats(res.data));
};
const handleLogout = () => {
// Cancel all pending requests
requestManager.cancelAll();
};
const refreshDashboard = () => {
// Cancel dashboard requests and reload
requestManager.cancelGroup("dashboard");
loadDashboard();
};
return (
<div>
<button onClick={refreshDashboard}>Refresh</button>
<button onClick={handleLogout}>Logout</button>
{/* Display active request count */}
<div>
Active Requests: {requestManager.getTotalActiveRequests()}
</div>
</div>
);
}5. Cleanup Hook - useRequestCleanup
import { useRequestCleanup } from "axios-cancel-groups";
function PageWrapper({ children }) {
// Cancel ALL requests when this component unmounts
useRequestCleanup();
return <div>{children}</div>;
}Axios - Frontend (Vanilla JS)
Create an Axios instance with request grouping support:
import { createAxiosWithRequestGroups, group } from "axios-cancel-groups";
// Create API instance with request manager
const { api, requestManager } = createAxiosWithRequestGroups({
axiosConfig: {
baseURL: "/api",
},
});
// Make grouped requests
async function fetchUserData() {
// Add requests to "page:/users" group
const usersPromise = api.get("/users", group("page:/users"));
const profilePromise = api.get("/profile", group("page:/users"));
const [users, profile] = await Promise.all([usersPromise, profilePromise]);
return { users, profile };
}
// Cancel all requests in a group (e.g., on route change)
function onRouteChange() {
requestManager.cancelGroup("page:/users");
}
// Cancel all pending requests
function onLogout() {
requestManager.cancelAll();
}
// Check active requests
console.log(requestManager.getGroupSize("page:/users")); // 0
console.log(requestManager.getTotalActiveRequests()); // 0React Router Example
import { useEffect } from "react";
import { useLocation } from "react-router-dom";
import { createAxiosWithRequestGroups } from "axios-cancel-groups";
const { api, requestManager } = createAxiosWithRequestGroups({
axiosConfig: { baseURL: "/api" },
});
function App() {
const location = useLocation();
useEffect(() => {
// Cancel previous page's requests when route changes
return () => {
requestManager.cancelGroup(`page:${location.pathname}`);
};
}, [location.pathname]);
return <div>...</div>;
}Vue Router Example
import { watch } from "vue";
import { useRoute } from "vue-router";
import { createAxiosWithRequestGroups } from "axios-cancel-groups";
const { api, requestManager } = createAxiosWithRequestGroups({
axiosConfig: { baseURL: "/api" },
});
export function useRouteCleanup() {
const route = useRoute();
watch(
() => route.path,
(newPath, oldPath) => {
if (oldPath) {
requestManager.cancelGroup(`page:${oldPath}`);
}
}
);
}Express - Backend
Add automatic request cancellation and timeout handling:
import express from "express";
import { cancelAndTimebox } from "axios-cancel-groups";
const app = express();
// Add middleware with 15-second hard timeout
app.use(cancelAndTimebox({ hardTimeoutMs: 15000 }));
// Use req.cancel.signal in your route handlers
app.get("/heavy", async (req, res) => {
const signal = req.cancel!.signal;
// Long-running operation
for (let i = 0; i < 100; i++) {
// Check if request was cancelled
if (signal.aborted) {
console.log("Request cancelled by client");
return;
}
await new Promise((resolve) => setTimeout(resolve, 200));
}
res.json({ ok: true });
});
// Database query example
app.get("/users", async (req, res) => {
try {
// Pass abort signal to database query (if supported)
const users = await db.query("SELECT * FROM users", {
signal: req.cancel!.signal,
});
res.json(users);
} catch (error) {
if (error.name === "AbortError") {
console.log("Query cancelled");
return;
}
throw error;
}
});
// File processing example
app.post("/process-file", async (req, res) => {
const signal = req.cancel!.signal;
const chunks = [];
for await (const chunk of fileStream) {
if (signal.aborted) {
console.log("File processing cancelled");
return;
}
chunks.push(processChunk(chunk));
}
res.json({ processed: chunks.length });
});
app.listen(3000, () => {
console.log("Server running on port 3000");
});Middleware Options
interface CancelAndTimeboxOptions {
/**
* Maximum request duration in milliseconds before sending 504 timeout
* @default undefined (no timeout)
*/
hardTimeoutMs?: number;
}When hardTimeoutMs is exceeded:
- Request's AbortController is aborted
- Response status is set to
504 Gateway Timeout - Connection is closed
- All event listeners and timers are cleaned up
API Reference
React Hooks
RequestCancelProvider
Context provider that wraps your app and manages request cancellation.
Props:
children: ReactNode- Your app componentsaxiosConfig?: AxiosRequestConfig- Optional Axios configuration
useRequestManager()
Returns the axios instance and request manager.
Returns:
{
api: AxiosInstance,
requestManager: RequestManager
}useAxios()
Returns the axios instance for making requests.
Returns: AxiosInstance
useGroupCancellation(groupKey, dependencies?)
Automatically cancels a group when dependencies change or component unmounts.
Parameters:
groupKey: string- The group key to canceldependencies?: any[]- Optional dependency array (like useEffect)
useRouteCancellation(location)
Cancels requests when the route/location changes.
Parameters:
location: string- Current location/route path
Returns: string - Group key for the current route (e.g., "route:/users")
useRequestCleanup()
Cancels all pending requests when the component unmounts.
Axios
createAxiosWithRequestGroups(options?)
Creates an Axios instance with request grouping support.
Parameters:
options.axiosConfig(optional): Axios configuration object
Returns:
{
api: AxiosInstance,
requestManager: RequestManager
}group(key: string)
Helper function to assign a group key to a request.
Example:
api.get("/users", group("page:/users"));RequestManager
Methods:
cancelGroup(groupKey: string): Cancel all requests in a groupcancelAll(): Cancel all pending requestsgetGroupSize(groupKey: string): Get number of active requests in a groupgetTotalActiveRequests(): Get total number of active requests
Express
cancelAndTimebox(options?)
Express middleware for automatic request cancellation and timeout handling.
Parameters:
options.hardTimeoutMs(optional): Maximum request duration in milliseconds
Adds to Request:
interface Request {
cancel?: AbortController;
}TypeScript
The package includes full TypeScript definitions:
import type {
GroupableConfig,
RequestManager,
CancelAndTimeboxOptions,
} from "axios-cancel-groups";
// Extended AxiosRequestConfig with group key
const config: GroupableConfig = {
url: "/api/users",
group: "page:/users",
};
// Express Request automatically includes cancel property
app.get("/route", (req, res) => {
req.cancel!.signal; // AbortSignal
});Testing
Run tests with Vitest:
# Run tests once
npm test
# Run tests in watch mode
npm run test:watch
# Run tests with coverage
npm run test:coverageTest Suite Coverage
The comprehensive test suite includes:
Unit Tests
axios.test.ts - Axios request grouping (13 tests)
- ✅ Axios instance creation with interceptors
- ✅ AbortController attachment to requests
- ✅ Group key handling and tracking
- ✅ Request cleanup on success and error
- ✅ Group cancellation operations
- ✅ Request manager operations (cancelAll, getGroupSize, etc.)
express.test.ts - Express middleware (20 tests)
- ✅ AbortController attachment to requests
- ✅ Client disconnect detection (aborted, close events)
- ✅ Hard timeout with 504 response
- ✅ Event listener cleanup
- ✅ Multiple cleanup trigger handling
- ✅ Integration scenarios
edge-cases.test.ts - Edge cases and corner scenarios (34 tests)
- ✅ Undefined, null, NaN, Infinity handling
- ✅ Special characters in group keys (unicode, emojis, etc.)
- ✅ Numeric, boolean, object, and Symbol group keys
- ✅ Empty strings and whitespace handling
- ✅ Very large values and timeout edge cases
- ✅ Multiple instance isolation
- ✅ Concurrent operations and race conditions
- ✅ Type safety edge cases
types.test.ts - TypeScript type safety (24 tests)
- ✅ GroupableConfig type extending AxiosRequestConfig
- ✅ RequestManager method signatures
- ✅ Express Request extension types
- ✅ Type inference and generic types
- ✅ Exported type accessibility
Total: 91 tests - All passing ✅
Build
npm run buildOutputs ESM and CJS formats to dist/ directory.
Release
This package uses release-it for automated releases.
Publishing a New Version
# Patch release (1.0.0 -> 1.0.1)
npm run release:patch
# Minor release (1.0.0 -> 1.1.0)
npm run release:minor
# Major release (1.0.0 -> 2.0.0)
npm run release:major
# Beta release (1.0.0 -> 1.0.1-beta.0)
npm run release:betaThe release process will:
- Run tests
- Build the package
- Update version in package.json
- Generate/update CHANGELOG.md
- Create a git tag
- Push to GitHub
- Publish to npm
- Create a GitHub release
Manual Release
For manual releases:
npm run releaseThis will prompt you to select the version type interactively.
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
MIT
