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
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 tolocalStorageand restored across page reloads. - Mutates
ctx.runtimein place (same object referencecreateMockHandlers()already closed over) whenever a consumer callssetRuntime(). No worker restart is needed, the next request picks up the newdelay/errorRateimmediately.
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/setModeabove. - Live
delay/errorRateinputs: editsctx.runtimethrough the provider. - An "API Docs" row (hidden entirely when
mock.config.js'sdocs.enabledisfalse, the same flagmpug docschecks — seemock.config.jsreference →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 fromctx.schemas(the same fully-parsed schema map the CLI reads), no server round-trip: opened as aBlobURL, 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 viastore.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 currentruntimeconfig.POST {baseUrl}/__mockingpug/runtime: patchesdelay/errorRate, mutating the samectx.runtimeobject referencecreateNextHandlers()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 mergeupdateRecord()a realPUT/PATCHuses, bypassingruntime.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-shotfailNext/delayNextoverride 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 asmpug docs'index.html, rendered live from the process's currentctxon every request rather than written to disk — always current with whatever schema the running dev server has loaded, no separate regenerate step. Returns a404(same as any unknown devtools route) whendocs.enabled: false, and the "API Docs" row is hidden in that case too, driven by adocsEnabledflag on the panel's rootGET {baseUrl}/__mockingpugresponse.
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.