Built for Next.js 16+

Message Queue
for Next.js

A tiny queue + actions runtime for Next.js. Simply dispatchEvent with CustomEvent. Small initial load (~1KB), requirements system, job deduplication, and status hooks.

~1 KB
initial load
Event Bridge only
~20 KB
total size
When fully loaded
0 KB
handlers initial
Load on-demand

Why NextMQ?

Built specifically for Next.js with simplicity and developer experience in mind

Simply dispatchEvent

Just use window.dispatchEvent() with CustomEvent. No complex APIs, no abstractions. The web platform you already know.

Small Initial Load

Only ~1KB initial load. Event Bridge loads first, processor and handlers load dynamically on-demand.

Requirements System

Gate jobs until requirements are met. Perfect for user consent, authentication, or feature flags.

Job Deduplication

Prevent duplicate jobs with dedupeKey. Perfect for analytics, tracking, and one-time actions.

Status Hooks

Track job status in real-time with useJobStatus hook. Perfect for updating UI based on job state.

Debouncing Built-In

Delay + Deduplication = automatic debouncing! Replace previous jobs and wait for inactivity. Perfect for search inputs and analytics.

Code Splitting

Handlers are automatically code-split. Only load what you need, when you need it.

Simply dispatchEvent

No complex APIs. No abstractions. Just the standard web platform you already know.

Standard CustomEvent API

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 window

No Learning Curve

Uses standard CustomEvent API you already know. No new concepts to learn.

Works Everywhere

Dispatch from React, vanilla JS, third-party scripts, or browser extensions.

Configurable Event Name

Use default 'nextmq' or choose your own. Perfect for multiple instances or avoiding conflicts.

Why Custom Event Names?

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!

Small Initial Load

Only ~1KB initial load. Everything else loads dynamically when needed.

~1 KB
Initial Load
Event Bridge only
~20 KB
Total Size
When fully loaded
0 KB
Handlers Initial
Load on-demand

How It Works

  • 1.Event Bridge (~1KB) loads immediately and starts listening for events
  • 2.Processor (~19KB) loads dynamically when NextMQClientProvider mounts
  • 3.Handlers (0KB initial) load on-demand when their job type is dispatched
// 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 splitting

Requirements System

Gate jobs until requirements are met. Perfect for user consent, authentication, or feature flags.

Gate Jobs Until Ready

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!

Use Cases

  • • User consent (GDPR, CCPA)
  • • Authentication status
  • • Feature flags
  • • API readiness
  • • Any conditional logic

Benefits

  • • No lost events
  • • Automatic processing
  • • Type-safe requirements
  • • Multiple requirements per job
  • • Works with deduplication

Debouncing: Delay + Deduplication

Combine delay with deduplication to get automatic debouncing. The same dedupeKeyprovides both behaviors: replaces queued jobs (debouncing) and skips completed jobs (deduplication).

Debouncing Built-In: Delay + Deduplication

Combine delay with dedupeKey to get automatic debouncing! Here's how it works:

How dedupeKey Works

  • If already queued:New job replaces the previous one (debouncing) - resets the delay timer
  • If already completed:New job is skipped (deduplication) - prevents duplicate execution

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
    }
  })
);

Use Cases

  • • Debouncing user input
  • • Rate limiting API calls
  • • Scheduled notifications
  • • Delayed animations
  • • Staggered batch processing

How It Works

  • • Delay is calculated from job creation time
  • • Jobs wait in queue until delay elapses
  • • With dedupeKey: replaces previous job (debouncing)
  • • Works seamlessly with requirements
  • • Non-blocking - other jobs can process
  • • Automatic retry scheduling

Example: Debounced Search (Delay + Deduplication)

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! 🎯

Job Deduplication

Prevent duplicate jobs from executing. Perfect for tracking events, analytics, and one-time actions.

Prevent Duplicate Execution

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 ✅

How It Works

  • • Tracks dedupeKey across full job lifecycle
  • • Checks both queue and completed jobs
  • • Prevents duplicates even if dispatched multiple times
  • • Persists for entire page lifecycle

Perfect For

  • • Analytics events
  • • Tracking pixels
  • • One-time actions
  • • Order completion
  • • Conversion tracking

How It Works

Tiny Event Bridge loads in first bytes (~1KB), then processor and handlers load dynamically

First Bytes Flow

Page Loads - Event Bridge Active

0ms

Tiny Event Bridge (~1KB) loads in first bytes, listening for events

Events Arrive Early

50ms

Events dispatched before processor is ready are buffered

Event Buffer (0 events)

Waiting for processor...

Processor Loads Dynamically

200ms

NextMQ processor and listener code-split and load on-demand

Buffer → Queue Transfer

250ms

Buffered events moved to queue when processor is ready

Requirements Check

300ms

Jobs gated until requirements are met

Handler Executes

400ms

Handler loaded dynamically and executed

Key Benefits

⚡ First Bytes Optimization

Event Bridge is only ~1KB and loads immediately. It starts listening for events right away, ensuring no events are lost.

📦 Smart Buffering

Events dispatched before the processor loads are automatically buffered and moved to the queue when ready.

🚀 Dynamic Loading

Processor and handlers load on-demand via code-splitting. Only what you need, when you need it.

✅ Requirements Gating

Jobs wait until their requirements are met. Perfect for consent, authentication, or feature flags.

How It Works in Code

// 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')

Installation

Get started in minutes

Terminal
npm install nextmq

Quick Start

Set up NextMQ in your Next.js app in 4 simple steps

💡 Tip: Add <NextMQDevTools /> to debug jobs in real-time

1

Set up EventBridge and Provider

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>
  );
}
2

Create a Processor

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;
3

Create Handlers

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
}
4

Dispatch Jobs

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' } }
//   })
// );

Status Hooks

Track job status in real-time with React hooks. Perfect for updating UI based on job state.

Real-Time Job Status Tracking

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>;
}

Status States

  • pending - Job queued
  • processing - Job executing
  • completed - Job finished
  • failed - Job errored

Use Cases

  • • Loading states
  • • Success notifications
  • • Error handling
  • • Progress indicators
  • • Optimistic UI updates
example.com/product

Fashion Product Image

New Arrival
FASHION BRAND

Premium Cotton T-Shirt

(128 reviews)
$49.99$79.9937% OFF
Size
Color
Free Shipping:On orders over $50
Returns:30-day return policy
Material:100% Organic Cotton

Example: Using useJobStatus Hook

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>;
}

Dynamic Loading Proof

Handlers are loaded on-demand. Portal dialogs, notifications, and more can be lazy-loaded easily.

Live Demo: Dynamic Code Loading

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:

  1. Open browser DevTools → Network tab
  2. Filter by "JS" to see JavaScript files
  3. Clear the network log (optional)
  4. Click a button above to dispatch a job
  5. Watch new chunk files appear (e.g., handlers_demoNotification.js)
  6. Each handler loads only once - subsequent clicks use the cached chunk

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);

Third-Party Script Integration

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!'
      }
    }
  })
);

Lazy-Load Portal Dialogs

Perfect for modals, dialogs, and overlays that are rarely used

Example: Portal Dialog Handler

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} />;
}

Call from Anywhere

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!' }
      }
    })
  );
})();

Benefits

  • Smaller initial bundle: Dialog code not loaded until needed
  • Better performance: Faster page loads, especially on mobile
  • Third-party friendly: External scripts can trigger dialogs without React context
  • Automatic code splitting: Next.js handles chunking automatically

Built for Next.js

Seamlessly integrates with Next.js Server Actions, Server Components, and the App Router

Server Actions Integration

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

  • • Server Action code is bundled with the handler (code-split)
  • • Only loads when the job type is dispatched
  • • No need for separate API routes or fetch calls
  • • Type-safe end-to-end with TypeScript
  • • Works with React Server Components and Server Actions

Built-in DevTools

Debug and monitor your jobs in real-time with the included DevTools component

Real-Time Job Monitoring

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 />
    </>
  );
}

Event Buffer

  • • Events waiting for processor
  • • Shows processor ready status
  • • Real-time updates

Job Queue

  • • Pending jobs with status
  • • Requirement status per job
  • • Processing state indicator

Features

  • • Floating panel in top-right corner (non-intrusive)
  • • Updates every 500ms when open
  • • Shows job IDs, types, payloads, and timestamps
  • • Visual indicators for requirement status
  • • Only renders on client (no hydration issues)
  • • Perfect for debugging during development

Ready to get started?

Start building with NextMQ today. Simple, standard, and built for Next.js.

NextMQ

MIT License • Built for Next.js