Creating custom dev tools with MSW
If you've ever been in a situation where the API for a feature you're adding to your frontend is not ready, then MSW is for you.
At work, this is often the case for me! I have been using MSW for integration testing but I never used it for developing because I was under the impression that it would intercept and reject any requests that weren't mocked.
I was wrong.
It doesn't reject unmocked requests, it just passes it along.
I thought back to Kent C. Dodd's post on dev tools and knew that dynamically mocking API's would really speed up my development workflow (it did).
Here's how I did it.
Making sure it's dev only
1// App.tsx2const DevTools = React.lazy(() => import("./DevTools"));34function App() {5 return (6 <>7 <Routes />8 {process.env.NODE_ENV === "development" ? (9 <React.Suspense fallback={null}>10 <DevTools />11 </React.Suspense>12 ) : null}13 </>14 );15}
Tada! Haha it would be great if that was all, but this is how I made sure the dev tools only loaded within the development environment. A simple dynamic component with a
suspense fallback.1null
This is the actual
implementation:1DevTools.tsx
1// DevTools.tsx2import * as React from "react";3import { setupWorker, graphql } from "msw";45export const mockServer = setupWorker();67const mocks = {8 users: [9 graphql.query("GetUsers", (req, res, ctx) => {10 // return fake data11 }),12 graphql.query("GetUser", (req, res, ctx) => {13 // return fake data14 }),15 ],16};1718function DevTools() {19 const [mocks, setMocks] = React.useState({});20 const mockServerReady = React.useRef(false);21 const activeMocks = React.useMemo(22 () =>23 Object.entries(mocks)24 // we filter out all unchecked inputs25 .filter(([, shouldMock]) => shouldMock)26 // since the map is an array of handlers27 // we want to flatten the array so that the final result isn't nested28 .flatMap(([key]) => mocks[key]),29 [mocks]30 );3132 React.useEffect(() => {33 mockServer.start().then(() => {34 mockServerReady.current = true;35 });3637 return () => {38 mockServer.resetHandlers();39 mockServer.stop();40 };41 }, []);4243 React.useEffect(() => {44 if (mockServerReady.current) {45 flushMockServerHandlers();46 }47 }, [state.mock]);4849 // if a checkbox was unchecked50 // we want to make sure that the mock server is no longer mocking those API's51 // we reset all the handlers52 // then add them to MSW53 function flushMockServerHandlers() {54 mockServer.resetHandlers();55 addHandlersToMockServer(activeMocks);56 }5758 function addHandlersToMockServer(handlers) {59 mockServer.use(...handlers);60 }6162 function getInputProps(name: string) {63 function onChange(event: React.ChangeEvent<HTMLInputElement>) {64 const apiToMock = event.target.name;65 const shouldMock = event.target.checked;6667 setState((prevState) => ({68 ...prevState,69 [apiToMock]: shouldMock,70 }));71 }7273 return {74 name,75 onChange,76 checked: state.mock[name] ?? false,77 };78 }7980 return (81 <div>82 {Object.keys(mocks).map((mockKey) => (83 <div key={mockKey}>84 <label htmlFor={mockKey}>Mock {mockKey}</label>85 <input {...getInputProps(mockKey)} />86 </div>87 ))}88 </div>89 );90}
Let's break that down.
Mock server
Inside the
file, I initialize the mock server and I add a map of all the API's I want to be able to mock and assign it to1DevTools.tsx
. In this example I'm using graphql, but you could easily replace that with whatever REST API you may be using.1mocks
1// DevTools.tsx2import { setupWorker, graphql } from "msw";34export const mockServer = setupWorker();56const mocks = {7 users: [8 graphql.query("GetUsers", (req, res, ctx) => {9 // return fake data10 }),11 graphql.query("GetUser", (req, res, ctx) => {12 // return fake data13 }),14 ],15};
UI
I make a checkbox for every key within
. The1mocks
initializes all the props for each checkbox. Each time a checkbox is checked, I'll update the state to reflect which API should be mocked.1getInputProps
1// DevTools.tsx23function DevTools() {4 const [mocks, setMocks] = React.useState({});56 function getInputProps(name: string) {7 function onChange(event: React.ChangeEvent<HTMLInputElement>) {8 const apiToMock = event.target.name;9 const shouldMock = event.target.checked;1011 setState((prevState) => ({12 ...prevState,13 [apiToMock]: shouldMock,14 }));15 }1617 return {18 name,19 onChange,20 checked: state.mock[name] ?? false,21 };22 }2324 return (25 <div>26 {Object.keys(mocks).map((mockKey) => (27 <div key={mockKey}>28 <label htmlFor={mockKey}>Mock {mockKey}</label>29 <input {...getInputProps(mockKey)} />30 </div>31 ))}32 </div>33 );34}
Dynamic API Mocking
This part has a little more to unpack.
1// DevTools.tsx2export const mockServer = setupWorker();34function DevTools() {5 const [mocks, setMocks] = React.useState({});6 const mockServerReady = React.useRef(false);7 const activeMocks = React.useMemo(8 () =>9 Object.entries(mocks)10 .filter(([, shouldMock]) => shouldMock)11 .flatMap(([key]) => mocks[key]),12 [mocks]13 );1415 React.useEffect(() => {16 mockServer.start().then(() => {17 mockServerReady.current = true;18 });1920 return () => {21 mockServer.resetHandlers();22 mockServer.stop();23 };24 }, []);2526 React.useEffect(() => {27 if (mockServerReady.current) {28 flushMockServerHandlers();29 }30 }, [state.mock]);3132 function flushMockServerHandlers() {33 mockServer.resetHandlers();34 addHandlersToMockServer(activeMocks);35 }3637 function addHandlersToMockServer(handlers) {38 mockServer.use(...handlers);39 }40}
First, we create a ref to track whether the mock server is ready.
1function DevTools() {2 const mockServerReady = React.useRef(false);3}
Then we create a list of all the active mocks to pass into MSW.
1function DevTools() {2 const mockServerReady = React.useRef(false);3 const activeMocks = React.useMemo(4 () =>5 Object.entries(mocks)6 .filter(([, shouldMock]) => shouldMock)7 .flatMap(([key]) => mocks[key]),8 [mocks]9 );10}
When the dev tools initialize, we want to start the server, and set the
ref to1mockServerReady
. When it unmounts, we reset all the handlers and stop the server.1true
1function DevTools() {2 const mockServerReady = React.useRef(false);3 const activeMocks = React.useMemo(4 () =>5 Object.entries(mocks)6 .filter(([, shouldMock]) => shouldMock)7 .flatMap(([key]) => mocks[key]),8 [mocks]9 );1011 React.useEffect(() => {12 mockServer.start().then(() => {13 mockServerReady.current = true;14 });1516 return () => {17 mockServer.resetHandlers();18 mockServer.stop();19 };20 }, []);21}
Finally, whenever we check a checkbox, we reset all the mocks and add whichever handlers are checked within
.1mocks
1function DevTools() {2 const mockServerReady = React.useRef(false);3 const activeMocks = React.useMemo(4 () =>5 Object.entries(mocks)6 .filter(([, shouldMock]) => shouldMock)7 .flatMap(([key]) => mocks[key]),8 [mocks]9 );1011 React.useEffect(() => {12 mockServer.start().then(() => {13 mockServerReady.current = true;14 });1516 return () => {17 mockServer.resetHandlers();18 mockServer.stop();19 };20 }, []);2122 React.useEffect(() => {23 if (mockServerReady.current) {24 flushMockServerHandlers();25 }26 }, [state.mock]);27}
That's all folks!