# What You’ll Build and Why It Matters#
React component contract testing means validating a component against a realistic set of API behaviors: success, empty states, validation errors, timeouts, and authorization failures. The focus is not only on rendering but on the contract between UI and backend.
This matters because UI regressions often come from “mock drift”: Storybook uses one fake response, tests use another, and production uses a third. The result is flaky UI, inconsistent states, and bugs that only show up after release.
This guide shows a practical setup where MSW handlers are the single source of truth for both Storybook and tests. You get living API mocks, reviewable in PRs, and enforced in CI.
If you want the broader testing pyramid and toolchain context, read our baseline strategy first: React testing strategy with Vitest, React Testing Library, and MSW.
# Prerequisites#
| Requirement | Version | Notes |
|---|---|---|
| Node.js | 18+ | LTS recommended |
| React | 18+ | Works with 19 too, but examples use 18 conventions |
| Storybook | 7.6+ | Examples assume modern Storybook config |
| MSW | 2+ | Uses http and HttpResponse APIs |
| Vitest | 1.5+ | Works with Vite-based React apps |
| React Testing Library | 14+ | For user-focused tests |
# What “Contract Testing” Means at Component Level#
Component contract testing is not about verifying backend schema correctness. It’s about verifying UI behavior given known API contracts.
A component contract includes:
| Contract element | Example | What to assert |
|---|---|---|
| Request shape | GET /api/orders?page=1 | Component requests expected URL and params |
| Response shape | items, total, pagination | UI renders data and pagination correctly |
| Error behavior | 401, 500, 422 | Correct messages, retry actions, and navigation |
| Timing | slow responses | Loading skeletons and disabled actions |
| Edge cases | empty arrays, missing optional fields | Empty state copy and fallback UI |
A good contract test suite usually covers 6 to 12 “UI-meaningful” API scenarios per feature. That is enough to prevent most regressions without exploding maintenance cost.
🎯 Key Takeaway: Treat MSW handlers as versioned, reviewable “API contracts” for the UI, not as throwaway test stubs.
# Folder Structure: One Source of Truth for Mocks#
The simplest way to prevent drift is to put handlers in a shared module and import them from both Storybook and test setup.
A practical structure:
| Path | Purpose |
|---|---|
src/mocks/handlers/ | Shared request handlers, grouped by domain |
src/mocks/scenarios/ | Composed handler sets for specific states |
src/mocks/browser.ts | MSW worker for Storybook and local dev |
src/mocks/server.ts | MSW server for unit and integration tests |
src/mocks/test-data/ | Factories and fixtures used by handlers |
src/components/... | Components and stories |
This separation keeps “what the API does” in handlers, and “what state we want” in scenarios.
# Step 1: Define a Domain Handler Set (Shared)#
Create a handler file per domain. Example: orders.
// src/mocks/handlers/orders.handlers.ts
import { http, HttpResponse, delay } from 'msw';
type Order = { id: string; status: 'paid' | 'pending'; totalCents: number };
const orders: Order[] = [
{ id: 'ord_1', status: 'paid', totalCents: 1299 },
{ id: 'ord_2', status: 'pending', totalCents: 4999 },
];
export const ordersHandlers = [
http.get('/api/orders', async () => {
await delay(150);
return HttpResponse.json({ items: orders, total: orders.length });
}),
];A few practical rules:
- 1Keep handlers close to how the real API behaves. If production returns
itemsandtotal, do not returndata. - 2Add small delays by default. Many UI bugs are timing-related and never appear with instant mocks.
- 3Use realistic identifiers and values, not
id: 1. You want to catch string formatting and copy edge cases.
⚠️ Warning: Avoid writing handlers that are “too perfect”. If your production API sometimes returns empty lists or partial optional fields, model that in scenarios. Perfect mocks produce false confidence.
# Step 2: Compose Scenarios Instead of Duplicating Handlers#
Scenarios are named sets of handlers that represent UI states. This is where contract testing becomes maintainable.
// src/mocks/scenarios/orders.scenarios.ts
import { http, HttpResponse, delay } from 'msw';
import { ordersHandlers } from '../handlers/orders.handlers';
export const ordersScenario = {
default: () => [...ordersHandlers],
empty: () => [
http.get('/api/orders', async () => {
await delay(100);
return HttpResponse.json({ items: [], total: 0 });
}),
],
serverError: () => [
http.get('/api/orders', async () => {
await delay(100);
return HttpResponse.json({ message: 'Internal error' }, { status: 500 });
}),
],
unauthorized: () => [
http.get('/api/orders', async () => {
await delay(100);
return HttpResponse.json({ message: 'Unauthorized' }, { status: 401 });
}),
],
};Why this helps:
- Storybook stories can map cleanly to scenarios: “OrdersList Empty”, “OrdersList Error”.
- Tests can reuse scenarios without rewriting mocks.
- When the API changes, you update scenarios and all consumers see the change.
# Step 3: MSW Setup for Tests (Vitest)#
Create a test server setup that is imported by your test runner.
// src/mocks/server.ts
import { setupServer } from 'msw/node';
import { ordersScenario } from './scenarios/orders.scenarios';
export const server = setupServer(...ordersScenario.default());Then wire it into Vitest.
// src/test/setup.ts
import '@testing-library/jest-dom/vitest';
import { afterAll, afterEach, beforeAll } from 'vitest';
import { server } from '../mocks/server';
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());This configuration does two important things:
onUnhandledRequest: 'error'turns missing mocks into failures. That’s a contract enforcement mechanism.resetHandlers()prevents cross-test contamination, reducing flakiness.
# Step 4: MSW Setup for Storybook (Same Handlers)#
In Storybook, use the MSW addon and load the same handler scenarios.
// .storybook/preview.ts
import type { Preview } from '@storybook/react';
import { initialize, mswLoader } from 'msw-storybook-addon';
import { ordersScenario } from '../src/mocks/scenarios/orders.scenarios';
initialize({ onUnhandledRequest: 'bypass' });
const preview: Preview = {
loaders: [mswLoader],
parameters: {
msw: {
handlers: ordersScenario.default(),
},
},
};
export default preview;The choice of onUnhandledRequest differs:
- In tests, you want “error” to enforce coverage.
- In Storybook, “bypass” can be acceptable while building new components, but teams often switch to “warn” once mature.
ℹ️ Note: Storybook uses the Service Worker environment, while Vitest uses the Node interceptor. The same handler definitions work in both because MSW abstracts the runtime differences.
# Step 5: Stories as Living Contract Documentation#
Now you can write stories that explicitly declare which scenario they represent. This makes your Storybook a contract catalog.
// src/components/OrdersList/OrdersList.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { OrdersList } from './OrdersList';
import { ordersScenario } from '../../mocks/scenarios/orders.scenarios';
const meta: Meta<typeof OrdersList> = {
title: 'Orders/OrdersList',
component: OrdersList,
};
export default meta;
type Story = StoryObj<typeof OrdersList>;
export const Default: Story = {
parameters: { msw: { handlers: ordersScenario.default() } },
};
export const Empty: Story = {
parameters: { msw: { handlers: ordersScenario.empty() } },
};
export const ServerError: Story = {
parameters: { msw: { handlers: ordersScenario.serverError() } },
};Practical benefits:
- Product and QA can review UI states without needing backend seed data.
- Developers can reproduce tricky error states instantly.
- The handler scenarios become a “living spec” for how the UI expects the API to behave.
If you maintain a design system, align these states with design tokens, loading patterns, and empty/error components. It’s easier to keep consistency when tokens and UI primitives are standardized: Design system tokens with Tailwind and Radix.
# Step 6: Contract Tests that Reuse Scenarios#
A contract test should not rebuild mocks. It should select scenarios and assert user-visible behavior.
Example using React Testing Library:
// src/components/OrdersList/OrdersList.test.tsx
import { render, screen } from '@testing-library/react';
import { server } from '../../mocks/server';
import { ordersScenario } from '../../mocks/scenarios/orders.scenarios';
import { OrdersList } from './OrdersList';
it('renders orders from the API', async () => {
server.use(...ordersScenario.default());
render(<OrdersList />);
expect(await screen.findByText('ord_1')).toBeInTheDocument();
expect(screen.getByText('ord_2')).toBeInTheDocument();
});
it('shows an empty state when there are no orders', async () => {
server.use(...ordersScenario.empty());
render(<OrdersList />);
expect(await screen.findByText(/no orders/i)).toBeInTheDocument();
});
it('shows an error message on server error', async () => {
server.use(...ordersScenario.serverError());
render(<OrdersList />);
expect(await screen.findByText(/try again/i)).toBeInTheDocument();
});A few rules that reduce flakiness:
- Assert on text users see, not implementation details.
- Use
findBy...for async content. - Keep tests focused on behavior per scenario, not every subcomponent.
# How Sharing Handlers Prevents Drift and Flaky UI#
Drift happens when you have multiple sources of truth:
- Tests mock
fetchwithitems: [...]. - Storybook returns
data: [...]. - Production returns
items: [...]plustotal.
The UI “works” in one environment and fails in another.
Sharing handlers prevents this because:
- Any response shape change is a single diff in
src/mocks/handlersorsrc/mocks/scenarios. - Storybook and tests both break when the contract breaks, which forces alignment.
- Reviewers can spot API-shape changes in PRs, not in bug reports.
In mature teams, handler changes are treated like API changes: they require review, and often require updating the UI and tests in the same PR.
# Add Contract Strictness: Fail on Unhandled Requests#
Strictness is the difference between “nice demo mocks” and contract testing.
Recommended defaults:
| Environment | onUnhandledRequest | Why |
|---|---|---|
| Vitest | error | Forces every request to be in the contract |
| Storybook local | warn or bypass | Helps dev speed, but warn is better long-term |
| CI Storybook tests | warn or error | Prevents silently missing handlers in reviewed states |
If your components use generated API clients, strict MSW coverage catches “surprise calls” early, such as a new endpoint being hit due to a refactor.
# Workflow: CI, Visual Review, and Regression Prevention#
A good contract testing workflow combines:
- 1Automated contract tests in Vitest.
- 2Visual review of stories for critical states.
- 3Regression prevention through snapshots and reviewed mock changes.
CI Pipeline Example (Practical and Fast)#
A pragmatic CI approach for a React app:
| Step | Typical runtime | What it catches |
|---|---|---|
| Typecheck and lint | 1 to 3 minutes | Unsafe refactors, rule violations |
| Unit plus contract tests | 2 to 6 minutes | UI-data regressions, error handling, loading states |
| Storybook build | 2 to 5 minutes | Broken stories, missing assets |
| Visual test run | 3 to 10 minutes | CSS regressions, layout shifts, theming issues |
To keep the pipeline fast, scope visual tests to key stories, not the entire library.
CI Commands (Example)#
These commands show a common baseline.
# tests
pnpm test --run
# build Storybook to ensure it compiles
pnpm storybook:buildFor visual checks, teams typically use Chromatic, Loki, or Playwright screenshot tests. The specifics depend on your stack, but the principle is the same: stories should represent the same MSW scenarios your tests use.
Regression Prevention: Tie Visual Stories to Scenarios#
Create a small “contract-critical” story set, such as:
- Default success state
- Empty state
- Error state
- Permission denied
- Slow network loading state
Those five states typically catch a large percentage of UI regressions, especially for data-heavy apps.
A practical naming convention:
| Story name | Scenario | Why it matters |
|---|---|---|
Default | success | Most-used path |
Empty | empty | Common edge case in new accounts |
ServerError | 500 | Messaging and retry behavior |
Unauthorized | 401 | Login redirect, permissions |
Loading | long delay | Skeletons, disabled actions |
Add a “Slow” Scenario for UI Timing Bugs#
Timing bugs are common in React: double spinners, flicker, disabled buttons re-enabled too early. Add a slow scenario and review it visually.
// src/mocks/scenarios/orders.scenarios.ts
import { http, HttpResponse, delay } from 'msw';
export const slowOrders = () => [
http.get('/api/orders', async () => {
await delay(2500);
return HttpResponse.json({ items: [], total: 0 });
}),
];Use it in a story named Loading, and assert in tests that skeletons render and actions are disabled while pending.
PR Review Checklist: Contract and UI Together#
A practical review process is to require:
- A handler or scenario change when the API behavior changes.
- A story update for any new UI state.
- A contract test for any bug fix that involved data-fetching or error handling.
This aligns well with a broader design and quality checklist: React design review checklist for performance, accessibility, maintainability.
💡 Tip: Put
src/mocks/**under CODEOWNERS review, similar to backend API definitions. It forces mock changes to be intentional and discourages “quick fixes” that break contract coverage.
# Practical Patterns That Scale Beyond One Component#
Pattern 1: “Feature Contract” Scenario Bundles#
For a feature page, bundle scenarios across multiple endpoints. Example: OrdersPage might call orders, user profile, and billing status.
Instead of scattering handlers across stories, create a feature-level scenario file that composes domain handlers.
| Approach | Pros | Cons |
|---|---|---|
| Per-component scenarios | Easy to start | Can duplicate cross-cutting requests |
| Per-feature scenarios | Matches real screens | Requires coordination across components |
| Hybrid | Best of both | Needs conventions and code review discipline |
Pattern 2: Data Factories for Stability#
Hardcoded fixtures get stale. Factories keep fixtures consistent and allow randomization when needed.
A pragmatic rule is: stable by default, randomized only in dedicated fuzz tests.
Pattern 3: Contract Coverage for Pagination, Sorting, and Filtering#
These are frequent regression sources because they rely on query params.
A simple approach:
- Create a handler that asserts query parameters and returns different payloads.
- Add one Storybook story per query variant you care about.
Even a small set of stories and tests here catches issues like wrong parameter names and broken sorting labels.
# Common Pitfalls and How to Avoid Them#
- 1
Duplicating handlers in stories and tests
Fix: shared handler modules plus scenario composition. - 2
Not failing on unhandled requests in tests
Fix: enforceonUnhandledRequest: 'error'so missing contracts are caught. - 3
Mocking response shapes that don’t match production
Fix: base handlers on real API responses, and keep them updated when backend changes. - 4
Too many stories without a review strategy
Fix: define a “contract-critical” subset for visual regression and keep the rest informational. - 5
Using instant responses everywhere
Fix: add default delay, and include at least one slow scenario per feature.
# Key Takeaways#
- Define MSW handlers once and share them between Storybook and tests to eliminate mock drift.
- Build named scenarios for success, empty, error, unauthorized, and slow states, then reuse them across stories and contract tests.
- Make tests strict with
onUnhandledRequest: 'error'to enforce UI-to-API contract coverage. - Treat Storybook stories as living contract documentation and run visual checks on a small, high-value subset in CI.
- Review changes to
src/mocks/**like production code, because handler changes effectively change the UI contract.
# Conclusion#
React component contract testing works best when your mocks are a shared, versioned asset. MSW plus Storybook gives you living API mocks that improve day-to-day development and significantly reduce flaky UI and late-stage regressions.
If you want help standardizing this across a React codebase, including Storybook governance, CI visual regression, and a stable MSW contract layer, reach out to Samioda and we’ll help you implement a maintainable testing workflow.
FAQ
Founder & Senior Developer at Samioda. 8+ years building React, Next.js, Flutter and n8n automation solutions for clients across Europe.
More in Web Development
All →React Data Table Patterns at Scale: Virtualization, Column Pinning, Filters, and Export (TanStack Table + Virtual)
Production-ready React data table patterns using TanStack Table and TanStack Virtual: state architecture, server-side pagination and sorting, debounced filters, column pinning, exports, and performance pitfalls.
Next.js B2B SaaS Admin Panel Architecture: RBAC, Audit Logs, Impersonation, and Safe Bulk Actions
A practical reference architecture for Next.js admin panels: permission modeling, audit logs, secure impersonation, and safety checklists for bulk actions, exports, and PII handling.
Building a Multi‑Step Wizard in Next.js App Router with Server Actions + Zod (No Extra API Layer)
Implement a production-grade Next.js multi step form using App Router Server Actions and Zod — with three state strategies (cookies, DB drafts, URL), accessible UX, optimistic transitions, and robust error handling without adding an API layer.
Need help with your project?
We build custom solutions using the technologies discussed in this article. Senior team, fixed prices.
Related Articles
React Testing Strategy in 2026: Vitest + React Testing Library + MSW for Confident Releases
A pragmatic React testing strategy for 2026 using Vitest, React Testing Library, and MSW. Learn a realistic test pyramid, reduce flakiness, and ship confidently with CI-ready patterns.
Offline-Friendly React Apps with TanStack Query: Persistence, Retries, and Optimistic UI (2026 Guide)
Build resilient offline-first UX with TanStack Query: React Query offline persistence, retry and backoff strategies, safe optimistic updates, partial offline patterns, and MSW testing.
Modern React Frontend Architecture: Feature-Based Modules, Boundaries, and Scalability
A practical guide to React frontend architecture feature-based modules: clear boundaries, shared layers, dependency rules, and a maintainable folder strategy for React and Next.js.