mockingpug
Guides

MockProvider & MockDevtools

Optional React components for worker lifecycle and live runtime editing

mockingpug/react exports two optional components on top of createMockHandlers(): <MockProvider> (owns the MSW worker's lifecycle and exposes a context) and <MockDevtools> (a floating panel that reads and writes that context). Neither is required: you can keep hand-writing setupWorker()/worker.start() as shown in the React guide if you don't want a devtools panel.

Setup

src/mocks/browser.tsx
import { setupWorker } from 'msw/browser';
import { MemoryStoreAdapter, generateAll, createMockHandlers } from 'mockingpug/react';
import { schemas, customDictionaries } from './schemas';

export async function createMockingpug() {
  const store = new MemoryStoreAdapter();
  await generateAll(schemas, store, { seed: 'my-app', customDictionaries });
  const ctx = {
    schemas,
    store,
    seed: 'my-app',
    customDictionaries,
    pagination: {
      strategy: 'page' as const,
      params: { page: 'page', limit: 'limit', offset: 'offset', cursor: 'cursor' },
      defaultLimit: 20,
      maxLimit: 100,
      envelope: true,
    },
  };
  const worker = setupWorker(...createMockHandlers(ctx, '/api'));
  return { ctx, worker };
}
import { MockProvider, MockDevtools } from 'mockingpug/react';

const { ctx, worker } = await createMockingpug();

<MockProvider worker={worker} ctx={ctx}>
  <App />
  <MockDevtools />
</MockProvider>;

Both components must be behind the same dev-only dynamic import() gate as startMocking() itself (see the React guide). Never import them at the top level of a file that ships to production.

<MockProvider>

interface MockProviderProps {
  children: ReactNode;
  worker: { start(options?: unknown): Promise<unknown>; stop(): void }; // duck-typed: an MSW setupWorker() result satisfies this
  ctx: QueryContext;
  initialMode?: 'mock' | 'off'; // default: 'mock'
  storageKey?: string | null;   // localStorage key for persisting the toggle; default: 'mockingpug:mode'; null disables persistence
}
  • Starts the worker when mode === 'mock', stops it when 'off'. The chosen mode is saved to localStorage and restored across page reloads.
  • Mutates ctx.runtime in place (same object reference createMockHandlers() already closed over) whenever a consumer calls setRuntime(). No worker restart is needed, the next request picks up the new delay/errorRate immediately.

Read the current state anywhere inside the provider with useMockContext():

const { mode, setMode, ctx, runtime, setRuntime, bypass, unbypass, isBypassed } = useMockContext();

Calling useMockContext() outside a <MockProvider> throws immediately (fail-fast) rather than returning undefined/a default silently.

<MockDevtools>

A floating collapsed button that expands into a settings panel with:

  • Mock network toggle: same as mode/setMode above.
  • Live delay/errorRate inputs: edits ctx.runtime through the provider.
  • An "API Docs" row (hidden entirely when mock.config.js's docs.enabled is false, the same flag mpug docs checks — see mock.config.js reference → docs.enabled) that opens a generated OpenAPI-based HTML reference of your mock's entire REST surface in a new tab. Generated entirely client-side from ctx.schemas (the same fully-parsed schema map the CLI reads), no server round-trip: opened as a Blob URL, not downloaded.
  • A "Mock Data" row that navigates to a second page listing every entity, its record count, and a per-entity bypass switch (the runtime half of bypass), plus a "reset all" button in that page's header, and "Export"/"Import" buttons right below it. "Export" downloads the entire store (every entity's { meta, records }) as one JSON file; "Import" reads such a file back in and restores it via store.save() per entity, refreshing the record counts shown in the list. Useful for capturing the exact data a bug reproduces on and handing it to a teammate, instead of describing "seed it like this" in words. An entity name in the file that doesn't match a current schema is silently skipped (no create-arbitrary-entity support here); invalid JSON shows an inline error instead of failing silently.

A text filter above the entity list narrows it by name (case-insensitive substring match) — useful once a real project has dozens of entities. The list itself is windowed: with a known fixed row height and container height, only the rows near the current scroll position are actually rendered (a small render-ahead margin either side), so scrolling through hundreds of entities/records doesn't degrade regardless of the exact count, no virtual-scroll dependency needed.

Clicking an entity in that list opens its stored records (reads ctx.store.load() directly, not over HTTP) in a separate floating window that can be dragged by its header and repositioned anywhere on the page; its own header has a reset button (store.deleteEntity() + generateAll() for just that entity) and a close button. Multiple entities' windows can be open at once; clicking an entity that's already open just brings its window to the front instead of opening a duplicate.

The pencil icon in that window's header makes the JSON view editable: change any field, click the checkmark to save. Saved edits go through the same merge PUT/PATCH already uses (so partial edits are fine, you don't need to retype the whole record), bypassing runtime.errorRate/delay — this is a devtools action on the data, not a request your app is making. An edited record survives the same things a manual PUT does (schema field additions, amount shrinking with _seed: false protection), but not a later change to that field's generator type — if you need a value that's fixed no matter what else changes in the schema, use fixtures instead. The cross icon next to it discards the edit without saving. Only existing records can be edited this way (matched by .id); adding or removing array entries in the textarea has no effect, use POST/DELETE (or the reset button) for that.

In the read-only view, each record has its own "Copy as curl" button (only shown for records with a resolvable .id), which copies a ready-to-run curl -X GET '<full URL>' for that exact record to the clipboard — no piecing the URL together by hand to poke it from a terminal or Postman.

Each entity's record window also has a "Fail next request" switch and a "Delay next" input + "Arm" button, right below its header. These arm a one-shot override for that entity: the very next request to it either throws (surfacing as a synthetic 500, same as runtime.errorRate would) or waits the given number of milliseconds before answering, and then the override disarms itself, so a normal runtime.errorRate/delay (or no runtime simulation at all) resumes on the request after that. This is for testing a specific error/loading state on demand (e.g. "what does my UI do if this one save request fails?") without dialing up errorRate globally and risking it firing on an unrelated request. The switch reflects whatever's actually armed on the server (it's read back via a "peek" call when the window opens), not just local UI state.

A "Requests" row (next to "Mock Data") shows the last 50 requests the mock actually answered — method, path, status, duration, and time — so you can tell whether a fetch() call reached the mock and what it got back without switching to the browser's Network tab. It polls once a second while open and has its own "Clear request log" button. Answered requests are recorded into ctx.requestLog (a ring buffer capped at 50 entries, oldest dropped first) right before the response goes out, so a synthetic runtime.errorRate failure still shows up with its real status. The devtools sub-API's own calls are never logged, only requests your app actually makes.

It renders nothing but the toggle button by default; the panel only appears once opened.

<MockDevtools> for Next.js

mockingpug/next/client exports its own <MockDevtools> for the Route Handler transport, since a Route Handler runs server-side and there's no client-side store or MockProvider context to read there:

import { MockDevtools } from 'mockingpug/next/client';

{process.env.NODE_ENV === 'development' && <MockDevtools />}

It's a separate subpath from mockingpug/next (rather than exported alongside createNextHandlers) because it's a 'use client' component: mockingpug/next's main entry also carries server-only, filesystem-touching code, and the two can't share a bundle under Next.js's App Router rules. You can still render it directly from a Server Component (e.g. page.tsx) without wrapping it in a client component yourself.

It talks to a small devtools sub-API the catch-all Route Handler already serves under {baseUrl}/__mockingpug/* (baseUrl defaults to /api, pass it explicitly if mock.config.js sets a different one) instead of reading a context directly:

  • GET {baseUrl}/__mockingpug: entity record counts plus the current runtime config.
  • POST {baseUrl}/__mockingpug/runtime: patches delay/errorRate, mutating the same ctx.runtime object reference createNextHandlers() reads from, so the change applies to the very next request with no restart.
  • GET {baseUrl}/__mockingpug/records/:entity: up to 10 current records, for the "Mock Data" preview window.
  • PUT {baseUrl}/__mockingpug/records/:entity/:id: applies an edit made in that window, through the same merge updateRecord() a real PUT/PATCH uses, bypassing runtime.errorRate/delay.
  • POST {baseUrl}/__mockingpug/reset/:entity: deletes and regenerates just that entity.
  • GET {baseUrl}/__mockingpug/requests: the last 50 answered requests, most-recent-first, for the "Requests" view.
  • POST {baseUrl}/__mockingpug/requests/clear: empties the request log.
  • GET {baseUrl}/__mockingpug/override/:entity: the one-shot fail/delay override currently armed for that entity, if any (without consuming it).
  • POST {baseUrl}/__mockingpug/override/:entity: arms a one-shot failNext/delayNext override for that entity's very next request.
  • GET {baseUrl}/__mockingpug/snapshot: the entire store as { entity: { meta, records } }, for the "Export" button.
  • POST {baseUrl}/__mockingpug/snapshot: restores entities from a previously exported snapshot, for the "Import" button; returns the updated entity counts.
  • GET {baseUrl}/__mockingpug/docs: the same generated HTML API reference as mpug docs' index.html, rendered live from the process's current ctx on every request rather than written to disk — always current with whatever schema the running dev server has loaded, no separate regenerate step. Returns a 404 (same as any unknown devtools route) when docs.enabled: false, and the "API Docs" row is hidden in that case too, driven by a docsEnabled flag on the panel's root GET {baseUrl}/__mockingpug response.

Same panel, same "Mock Data" list page/draggable floating record windows, same editable JSON viewer, same request log, same one-shot fail/delay override and Export/Import snapshot controls, and the same per-record "Copy as curl" button as the React version (baseUrl is what it uses to build the URL, no extra route needed — it's built client-side from window.location.origin + baseUrl + the record's .id). The "API Docs" button works the same way from the user's perspective, but unlike React's entirely-client-side generation, this transport serves it live from the route above, since a Route Handler runs server-side. There's no mock-network toggle or per-entity bypass switch built into this panel: both are React/MSW-specific concepts, and a Route Handler is the real server, so there's nothing to switch off or bypass at the panel level. See the Next.js guide for how to switch mock ↔ real instead: a build-time process.env-driven rewrites()/guard (the default, works everywhere), or two separate opt-in recipes for finer control — createProxyHandler() (per-request mock/real decision in the Route Handler itself) and createLiveToggleMiddleware() (a cookie-driven toggle you wire up with your own UI and middleware.ts, no rebuild needed).

errorRate: 1 can hide the panel itself

The devtools sub-API ({baseUrl}/__mockingpug/*, every request the panel itself makes) is a guaranteed invariant, covered by a regression test: it is never subject to runtime.errorRate/delay, no matter how it's configured. So errorRate: 1 never blocks the panel directly. What it can do is take down the page around it: ctx.runtime.errorRate lives on the server process, not in the browser, and if the page that renders <MockDevtools> does any of its own server-side data fetching (a normal request to a mocked entity, not a devtools call) without an error boundary around it, that whole route fails to render — the panel never gets a chance to mount, even though it was never itself in danger. Wrap that data fetching in an error boundary (or a try/catch for a Server Component) to keep the route rendering regardless of errorRate, or as a fallback, restart the dev server (or redeploy): ctx.runtime is in-memory only, so the fresh process reloads errorRate: 0 (or your mock.config.js default) from scratch, nothing is persisted or corrupted. See mock.config.js reference → runtime.errorRate for more.

Reference

Full prop/context shapes are in mockingpug/react's type exports: MockProviderProps, MockContextValue, MockMode, MockWorker. The Next.js devtools component's props (MockDevtoolsProps) are exported from mockingpug/next.

On this page