A tiny queue + actions runtime for Next.js. Simply dispatchEvent with CustomEvent. Small initial load (~1KB), requirements system, job deduplication, and status hooks.
Built specifically for Next.js with simplicity and developer experience in mind
Just use window.dispatchEvent() with CustomEvent. No complex APIs, no abstractions. The web platform you already know.
Only ~1KB initial load. Event Bridge loads first, processor and handlers load dynamically on-demand.
Gate jobs until requirements are met. Perfect for user consent, authentication, or feature flags.
Prevent duplicate jobs with dedupeKey. Perfect for analytics, tracking, and one-time actions.
Track job status in real-time with useJobStatus hook. Perfect for updating UI based on job state.
Delay + Deduplication = automatic debouncing! Replace previous jobs and wait for inactivity. Perfect for search inputs and analytics.
Handlers are automatically code-split. Only load what you need, when you need it.
No complex APIs. No abstractions. Just the standard web platform you already know.
NextMQ uses the native CustomEvent API. No learning curve, no vendor lock-in. Just standard web APIs. The event name is configurable - use the default 'nextmq' or choose your own.
import { NEXTMQ_EVENT_NAME } from 'nextmq';
// Option 1: Use the default event name (convenience constant)
window.dispatchEvent(
new CustomEvent(NEXTMQ_EVENT_NAME, {
detail: {
type: 'cart.add',
payload: { ean: '123', quantity: 1 }
}
})
);
// Option 2: Use any custom event name you want
// Just configure it in NextMQRootClientEventBridge:
<NextMQRootClientEventBridge eventName="myApp" />
// Then dispatch with the same name:
window.dispatchEvent(
new CustomEvent('myApp', {
detail: { type: 'cart.add', payload: { ean: '123' } }
})
);
// Use any event name: 'nextmq', 'myApp', 'helloMcNerd',
// 'app:jobs', 'shop:actions', 'analytics:track', or your own!
// Works from anywhere:
// - React components
// - Vanilla JavaScript
// - Third-party scripts
// - Browser extensions
// - Any code that can access windowUses standard CustomEvent API you already know. No new concepts to learn.
Dispatch from React, vanilla JS, third-party scripts, or browser extensions.
Use default 'nextmq' or choose your own. Perfect for multiple instances or avoiding conflicts.
Multiple Instances
Run multiple NextMQ instances in the same app with different event names. Perfect for micro-frontends or modular architectures.
Avoid Conflicts
Prevent conflicts with other libraries or third-party scripts that might use similar event names. Namespace your events.
Integration
Integrate with existing systems that have their own event naming conventions. Use their names directly.
Testing & Isolation
Isolate event streams for testing, mocking, or staging environments. Each environment can use different event names.
// Example: Multiple NextMQ instances with different event names
// Main app events
<NextMQRootClientEventBridge eventName="app:invoke" />
<NextMQClientProvider processor={mainProcessor} />
// Admin panel events (separate instance)
<NextMQRootClientEventBridge eventName="admin:invoke" />
<NextMQClientProvider processor={adminProcessor} />
// Analytics events (separate instance)
<NextMQRootClientEventBridge eventName="analytics:invoke" />
<NextMQClientProvider processor={analyticsProcessor} />
// Each instance listens to its own event name
// No conflicts, complete isolation!Only ~1KB initial load. Everything else loads dynamically when needed.
// 1. Event Bridge loads first (~1KB)
<NextMQRootClientEventBridge />
// ✅ Listening for events immediately
// 2. Processor loads dynamically (~19KB)
<NextMQClientProvider processor={processor} />
// ✅ Code-split, loads when provider mounts
// 3. Handlers load on-demand (0KB initial)
// Only when job type is dispatched
await import('./handlers/cartAdd');
// ✅ Perfect for code splittingGate jobs until requirements are met. Perfect for user consent, authentication, or feature flags.
Jobs with unmet requirements wait in the queue. Once requirements are met, they automatically process.
import { setRequirement } from 'nextmq';
// Dispatch a job with requirements
window.dispatchEvent(
new CustomEvent('nextmq', {
detail: {
type: 'analytics.track',
payload: { event: 'page_view' },
requirements: ['necessaryConsent'] // ⏳ Job waits here
}
})
);
// Later, when user gives consent
setRequirement('necessaryConsent', true);
// ✅ Job automatically processes!Combine delay with deduplication to get automatic debouncing. The same dedupeKeyprovides both behaviors: replaces queued jobs (debouncing) and skips completed jobs (deduplication).
Combine delay with dedupeKey to get automatic debouncing! Here's how it works:
The same dedupeKey provides both behaviors automatically!
Perfect for search inputs, analytics tracking, and any scenario where you want to wait for inactivity.
// True debouncing: delay + dedupeKey
// Multiple rapid calls → only the last one executes after delay
window.dispatchEvent(
new CustomEvent('nextmq', {
detail: {
type: 'analytics.track',
payload: { event: 'search', query: 'nextjs' },
dedupeKey: 'search-analytics', // Same key = debounce
delay: 500 // ⏳ Wait 500ms after last call
}
})
);
// Schedule notification for 2 seconds later (no dedupeKey = no debounce)
window.dispatchEvent(
new CustomEvent('nextmq', {
detail: {
type: 'notification.show',
payload: { message: 'Welcome!' },
delay: 2000 // ⏳ Wait 2 seconds before processing
}
})
);import { NEXTMQ_EVENT_NAME } from 'nextmq';
function SearchInput() {
const handleInput = (e) => {
// Dispatch with delay + dedupeKey = automatic debouncing!
window.dispatchEvent(
new CustomEvent(NEXTMQ_EVENT_NAME, {
detail: {
type: 'search.perform',
payload: { query: e.target.value },
dedupeKey: 'search-query', // Same key = replaces previous job (enables debouncing)
delay: 300 // Debounce: wait 300ms after last keystroke before processing
}
})
);
};
return <input onChange={handleInput} />;
}
// User types "n", "e", "x", "t" rapidly:
// - "n" → queued, waits 300ms (dedupeKey: "search-query")
// - "e" → replaces "n" (debouncing), waits 300ms
// - "x" → replaces "e" (debouncing), waits 300ms
// - "t" → replaces "x" (debouncing), waits 300ms
// → Only "next" search executes after 300ms of inactivity
//
// If user types again after search completes:
// - New job with same dedupeKey → skipped (deduplication)
// Perfect debouncing + deduplication! 🎯Prevent duplicate jobs from executing. Perfect for tracking events, analytics, and one-time actions.
Use dedupeKey to ensure a job only executes once per page lifecycle. Jobs with the same dedupeKey are automatically skipped if already processed or queued.
// Order completion tracking - only execute once
window.dispatchEvent(
new CustomEvent('nextmq', {
detail: {
type: 'analytics.orderCompleted',
payload: {
orderId: 'ORD-12345',
total: 99.99
},
dedupeKey: 'order-completed-ORD-12345' // ✅ Unique per order
}
})
);
// If called again (page re-render, navigation, etc.)
// The job is automatically skipped ✅Tiny Event Bridge loads in first bytes (~1KB), then processor and handlers load dynamically
Tiny Event Bridge (~1KB) loads in first bytes, listening for events
Events dispatched before processor is ready are buffered
Event Buffer (0 events)
Waiting for processor...
NextMQ processor and listener code-split and load on-demand
Buffered events moved to queue when processor is ready
Jobs gated until requirements are met
Handler loaded dynamically and executed
Event Bridge is only ~1KB and loads immediately. It starts listening for events right away, ensuring no events are lost.
Events dispatched before the processor loads are automatically buffered and moved to the queue when ready.
Processor and handlers load on-demand via code-splitting. Only what you need, when you need it.
Jobs wait until their requirements are met. Perfect for consent, authentication, or feature flags.
// 1. Event Bridge loads first (~1KB)
<NextMQRootClientEventBridge />
// 2. Events arrive early - buffered automatically
window.dispatchEvent(new CustomEvent('nextmq', ...))
// (or your custom event name)
// 3. Processor loads dynamically (code-split)
<NextMQClientProvider processor={processor} />
// 4. Buffered events → Queue (automatic)
setProcessEventCallback() processes buffer
// 5. Requirements checked, handlers load on-demand
await import('./handlers/cartAdd')
Get started in minutes
npm install nextmqSet up NextMQ in your Next.js app in 4 simple steps
💡 Tip: Add <NextMQDevTools /> to debug jobs in real-time
Add NextMQ components to your root layout. Optionally customize the event name.
// app/layout.tsx
import { NextMQRootClientEventBridge, NextMQClientProvider } from 'nextmq';
import processor from './processors';
export default function RootLayout({ children }) {
return (
<html>
<body>
{/* Default: listens for 'nextmq' */}
<NextMQRootClientEventBridge />
{/* Or use a custom event name: */}
{/* <NextMQRootClientEventBridge eventName="myApp" /> */}
<NextMQClientProvider processor={processor}>
{children}
</NextMQClientProvider>
</body>
</html>
);
}Route jobs to code-split handlers
// app/processors.ts
import type { Processor } from 'nextmq';
const processor: Processor = async (job) => {
switch (job.type) {
case 'cart.add':
const handler = await import('./handlers/cartAdd');
return handler.default(job);
default:
console.warn('Unknown job type:', job.type);
}
};
export default processor;Write handlers that can return JSX
// app/handlers/cartAdd.tsx
import type { Job } from 'nextmq';
export default async function cartAddHandler(
job: Job<{ ean: string; quantity: number }>
) {
// Your handler logic
return <div>Added to cart!</div>; // Optional: return JSX
}Send jobs via CustomEvent. Use NEXTMQ_EVENT_NAME constant or your custom event name.
import { NEXTMQ_EVENT_NAME } from 'nextmq';
// Option 1: Use the default event name (convenience)
window.dispatchEvent(
new CustomEvent(NEXTMQ_EVENT_NAME, {
detail: {
type: 'cart.add',
payload: { ean: '123', quantity: 1 },
requirements: ['necessaryConsent'], // Optional
dedupeKey: 'cart-add-123', // Optional: prevent duplicates
delay: 500 // Optional: delay in milliseconds
}
})
);
// Option 2: Use your custom event name (if configured)
// window.dispatchEvent(
// new CustomEvent('myApp', { // matches eventName prop
// detail: { type: 'cart.add', payload: { ean: '123' } }
// })
// );Track job status in real-time with React hooks. Perfect for updating UI based on job state.
Use the useJobStatus hook to track job status in your React components. Perfect for showing loading states, success messages, or error handling.
import { useJobStatus, useNextmq } from 'nextmq';
function AddToCartButton({ productId }) {
const [jobId, setJobId] = useState<string | null>(null);
const { status, result, error } = useJobStatus(jobId);
const queue = useNextmq();
const handleClick = () => {
const newJobId = queue.enqueue('cart.add', {
ean: productId,
quantity: 1
});
if (newJobId) setJobId(newJobId);
};
if (status === 'processing') return <Spinner />;
if (status === 'failed') return <Error error={error} />;
if (status === 'completed') return <Success data={result} />;
return <button onClick={handleClick}>Add to Cart</button>;
}pending - Job queuedprocessing - Job executingcompleted - Job finishedfailed - Job erroredFashion Product Image
import { useJobStatus, useNextmq } from 'nextmq';
function AddToCartButton({ productId }) {
const [jobId, setJobId] = useState<string | null>(null);
const { status, result, error } = useJobStatus(jobId);
const queue = useNextmq();
const handleClick = () => {
// Enqueue job and get job ID
const newJobId = queue.enqueue('cart.add', {
ean: productId,
quantity: 1
});
if (newJobId) {
setJobId(newJobId);
}
};
if (status === 'processing') return <Spinner />;
if (status === 'failed') return <Error error={error} />;
if (status === 'completed') return <Success data={result} />;
return <button onClick={handleClick}>Add to Cart</button>;
}Handlers are loaded on-demand. Portal dialogs, notifications, and more can be lazy-loaded easily.
Click the buttons below to dispatch jobs. Each handler is loaded dynamically when first used. Open DevTools → Network tab to see chunks loading in real-time.
How to Verify Dynamic Loading:
handlers_demoNotification.js)What's Happening Behind the Scenes:
// 1. You click a button, which dispatches:
window.dispatchEvent(
new CustomEvent('nextmq', {
detail: { type: 'demo.notification', payload: {...} }
})
);
// 2. Processor routes to handler (dynamic import)
const handler = await import('./handlers/demoNotification');
// ↑ This triggers Next.js to load the chunk file
// 3. Handler chunk loads (visible in Network tab)
// File: _next/static/chunks/handlers_demoNotification-[hash].js
// 4. Handler executes and returns JSX (rendered automatically)
return handler.default(job);NextMQ can be called from anywhere, even external scripts and partner integrations. No React context needed - just dispatch a CustomEvent.
// Example: Third-party analytics script
// Uses your configured event name (default: 'nextmq')
(function() {
// This script can trigger NextMQ handlers from anywhere
window.dispatchEvent(
new CustomEvent('nextmq', { // or your custom event name
detail: {
type: 'analytics.track',
payload: {
event: 'page_view',
url: window.location.href
}
}
})
);
})();
// Example: Partner widget integration
// Partner's script can trigger your portal dialogs
// Works with any event name you configure
window.dispatchEvent(
new CustomEvent('nextmq', { // matches your NextMQRootClientEventBridge config
detail: {
type: 'portal.show',
payload: {
title: 'Special Offer',
content: 'Get 20% off today!'
}
}
})
);Perfect for modals, dialogs, and overlays that are rarely used
This handler (and its React components) are only loaded when the dialog is actually needed.
// app/handlers/portalDialog.tsx
import type { Job } from 'nextmq';
import { createPortal } from 'react-dom';
// Heavy dialog component - only loaded when needed
function Dialog({ title, content, onClose }: DialogProps) {
return createPortal(
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg p-6 max-w-md">
<h2>{title}</h2>
<p>{content}</p>
<button onClick={onClose}>Close</button>
</div>
</div>,
document.body
);
}
export default async function portalDialogHandler(
job: Job<{ title: string; content: string }>
) {
// This entire handler chunk is loaded dynamically
// Only when 'portal.dialog' job is dispatched
return <Dialog title={job.payload.title} content={job.payload.content} />;
}Trigger portal dialogs from anywhere in your app, or even from third-party scripts.
// From your React component
window.dispatchEvent(
new CustomEvent('nextmq', {
detail: {
type: 'portal.dialog',
payload: {
title: 'Welcome!',
content: 'This dialog was lazy-loaded.'
}
}
})
);
// From a third-party script (e.g., analytics, partner widget)
(function() {
// Partner's script can trigger your dialogs
window.dispatchEvent(
new CustomEvent('nextmq', {
detail: {
type: 'portal.dialog',
payload: { title: 'Special Offer', content: '20% off!' }
}
})
);
})();Seamlessly integrates with Next.js Server Actions, Server Components, and the App Router
Handlers can call Server Actions directly. The handler code (including Server Action imports) is code-split and only loaded when needed.
Server Action:
// app/actions/cart.ts
'use server';
export async function addToCart(ean: string, quantity: number) {
// Server-side logic - database, validation, etc.
const cart = await db.cart.add({ ean, quantity });
return { success: true, cartId: cart.id };
}Handler (code-split):
// app/handlers/cartAdd.tsx
import type { Job } from 'nextmq';
import { addToCart } from '../actions/cart'; // Server Action
export default async function cartAddHandler(
job: Job<{ ean: string; quantity: number }>
) {
// Call Server Action - works seamlessly!
const result = await addToCart(
job.payload.ean,
job.payload.quantity
);
if (result.success) {
return <Notification>Added to cart!</Notification>;
}
return <Error>Failed to add</Error>;
}Why This Matters
Debug and monitor your jobs in real-time with the included DevTools component
Add <NextMQDevTools /> to any page to see live updates of your job queue, event buffer, and requirement status.
Usage:
import { NextMQDevTools } from 'nextmq';
export default function Page() {
return (
<>
<NextMQDevTools />
<YourPageContent />
</>
);
}Features
Start building with NextMQ today. Simple, standard, and built for Next.js.