Honest split first: msw-storybook-addon is the community standard and it's in good shape โ v3.0.0 shipped July 2026 with support for Storybook โฅ9 and MSW 2. If your components hard-code a production URL and you want to intercept it transparently, offline, with zero latency, use it; this page won't pretend otherwise. But there's a second way to drive data-dependent stories that involves no handler code, no mockServiceWorker.js file, no loader config at all: point the component at a hosted mock endpoint and make each story a different query string. We built it and verified everything below on Storybook 10.5.10 (@storybook/react-vite), React 19.2, Vite 8.2, Node 22 โ the four stories render exactly as described, checked in a real browser against a static storybook build.
A typical fetching component takes its endpoint from a prop (or an env var). Then the story matrix is nothing but URLs:
// ProductList.stories.jsx โ this exact file is what we verified
import { ProductList } from './ProductList';
const BASE = 'https://mockbird.mockbird.workers.dev/m/demo';
export default { title: 'ProductList', component: ProductList };
export const Default = {
args: { url: `${BASE}/products?limit=5` },
};
export const Loading = { // 3 real seconds of skeleton
args: { url: `${BASE}/products?limit=5&mock_delay=3000` },
};
export const ErrorState = { // real HTTP 500 + JSON error body
args: { url: `${BASE}/products?mock_status=500` },
};
export const Empty = { // filter that matches nothing โ []
args: { url: `${BASE}/products?category=none-such` },
};
That's the entire setup. No .storybook/preview changes, no initialize(), no worker file in staticDirs, no handlers to keep in sync with your API. The /m/demo project above is our shared public playground โ those four stories work in your Storybook right now, unchanged, without signing up for anything.
What each param did when we ran it:
| Story | URL trick | Verified behavior |
|---|---|---|
| Default | ?limit=5 | 5 seeded products, stable ids |
| Loading | ?mock_delay=3000 | response held ~3s (we measured 3.28s wall-clock) โ the skeleton state is real, watchable, and demo-able to a designer |
| ErrorState | ?mock_status=500 | genuine HTTP 500 with a JSON body โ exercises your actual !r.ok branch |
| Empty | ?category=none-such | [] โ the empty-state render, no fixture file |
One thing that does not work: ?limit=0 is treated as unset and returns the default page, not an empty array. Use a filter that matches nothing (as above), or โ cleaner โ an empty snapshot on your own project, next section.
Filters get you an empty list; snapshots get you an entire alternate dataset per story โ "empty everything", "edge-case names", "bug #412 repro" โ all served from one project, read-only, without touching live data. Create your own project (one curl, no signup) and freeze the states once:
# 1. one seeded e-commerce backend
curl -s -X POST https://mockbird.mockbird.workers.dev/api/projects \
-H 'Content-Type: application/json' -d '{"name":"storybook","preset":"ecommerce"}'
# โ { "id": "<project>", "adminKey": "<key>", ... }
# 2. freeze the seeded data as "baseline"
curl -s -X POST https://mockbird.mockbird.workers.dev/api/projects/<project>/snapshots \
-H 'x-admin-key: <key>' -H 'Content-Type: application/json' -d '{"name":"baseline"}'
# 3. wipe each resource (zero-count reseed), freeze as "empty", restore baseline
curl -s -X POST https://mockbird.mockbird.workers.dev/api/projects/<project>/resources/products \
-H 'x-admin-key: <key>' -H 'Content-Type: application/json' -d '{"seed":0}'
curl -s -X POST https://mockbird.mockbird.workers.dev/api/projects/<project>/snapshots \
-H 'x-admin-key: <key>' -H 'Content-Type: application/json' -d '{"name":"empty"}'
curl -s -X POST https://mockbird.mockbird.workers.dev/api/projects/<project>/snapshots/baseline/restore \
-H 'x-admin-key: <key>'
Now a story pins its scenario with one more query param โ we ran this flow end-to-end while writing the page (live endpoint kept serving the restored baseline; the pinned URL returned []):
export const Empty = {
args: { url: `${BASE}/products?mock_snapshot=empty` },
};
Snapshot reads are read-only and don't disturb the live dataset, so a colleague clicking through your deployed Storybook can't break the fixtures. Full recipe (including per-scenario snapshots for tests) in the deterministic test data guide.
| You want a story for | URL |
|---|---|
| Slow-network skeleton | ?mock_delay=3000 (up to 5000ms) |
| Any HTTP error | ?mock_status=500 โ or 401, 403, 429โฆ |
| Error then recovery (retry UI) | ?mock_seq=500,200&mock_seq_key=story-retry |
| Pagination controls | ?_page=2&_limit=5 (+ X-Total-Count header) |
| Sparse fields / long strings | edit records in the dashboard, snapshot it |
| Flaky network demo | ?mock_chaos=0.3 โ 30% of requests fail randomly |
| msw-storybook-addon | Mockbird | |
|---|---|---|
| Setup | install msw + addon, generate mockServiceWorker.js into your static dir, register the loader in preview, write handlers | none โ a story arg is a URL |
| Mocks your real production URL transparently | yes โ component code untouched | no โ component must take a base URL (prop or env), which is good practice anyway |
| Works offline / zero latency | yes | no โ real network call |
| Per-story response logic | anything you can write in JS handlers | query params + custom routes for fixed payloads |
| Fixtures live in | handler code you maintain and keep honest | a hosted dataset โ seeded, imported (OpenAPI, db.json, CSV, HAR), or hand-edited; snapshots freeze it |
| Stateful (a play function can POST and re-read) | hand-rolled handler state | default โ writes persist |
| Same data outside the Storybook tab | no โ the mock exists only where the worker runs | yes โ curl, tests, Playwright, a teammate's machine, the deployed preview all hit the same URL |
| Request cap | none | 10,000/project/day (demo: 50k shared) |
They also compose: keep the addon for stories that must intercept a hard-coded production URL, and point everything else at a Mockbird base URL via VITE_API_URL โ the pattern most component libraries already use.
curl 'https://mockbird.mockbird.workers.dev/m/demo/products?limit=5'
curl -i 'https://mockbird.mockbird.workers.dev/m/demo/products?mock_status=500'
curl 'https://mockbird.mockbird.workers.dev/m/demo/products?category=none-such'
All against the shared demo project (resets daily; CORS is open, so browser fetch() from any origin โ including your local Storybook โ just works). Or create your own with one click: open the dashboard with an e-commerce preset.
Written by the Mockbird maker โ bias disclosed. Where msw-storybook-addon genuinely wins: offline work, zero latency, arbitrary JS in handlers, and transparent interception of the exact URL your production code calls. It's actively maintained and we'd use it for those cases. When you'd rather have zero mock code in the repo and states you can hand to a designer as links, that's us.
Full API reference in the docs. More guides: mock API for React ยท MSW alternative ยท testing loading & error states ยท deterministic test data ยท mock APIs in Vitest ยท Playwright. Create your API โ