October 29, 2020 | 4 minutes read

Creating custom dev tools with MSW

#react#msw#webdev#tooling

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.tsx
2const DevTools = React.lazy(() => import("./DevTools"));
3
4function 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

1null
suspense fallback.

This is the actual

1DevTools.tsx
implementation:

1// DevTools.tsx
2import * as React from "react";
3import { setupWorker, graphql } from "msw";
4
5export const mockServer = setupWorker();
6
7const mocks = {
8 users: [
9 graphql.query("GetUsers", (req, res, ctx) => {
10 // return fake data
11 }),
12 graphql.query("GetUser", (req, res, ctx) => {
13 // return fake data
14 }),
15 ],
16};
17
18function 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 inputs
25 .filter(([, shouldMock]) => shouldMock)
26 // since the map is an array of handlers
27 // we want to flatten the array so that the final result isn't nested
28 .flatMap(([key]) => mocks[key]),
29 [mocks]
30 );
31
32 React.useEffect(() => {
33 mockServer.start().then(() => {
34 mockServerReady.current = true;
35 });
36
37 return () => {
38 mockServer.resetHandlers();
39 mockServer.stop();
40 };
41 }, []);
42
43 React.useEffect(() => {
44 if (mockServerReady.current) {
45 flushMockServerHandlers();
46 }
47 }, [state.mock]);
48
49 // if a checkbox was unchecked
50 // we want to make sure that the mock server is no longer mocking those API's
51 // we reset all the handlers
52 // then add them to MSW
53 function flushMockServerHandlers() {
54 mockServer.resetHandlers();
55 addHandlersToMockServer(activeMocks);
56 }
57
58 function addHandlersToMockServer(handlers) {
59 mockServer.use(...handlers);
60 }
61
62 function getInputProps(name: string) {
63 function onChange(event: React.ChangeEvent<HTMLInputElement>) {
64 const apiToMock = event.target.name;
65 const shouldMock = event.target.checked;
66
67 setState((prevState) => ({
68 ...prevState,
69 [apiToMock]: shouldMock,
70 }));
71 }
72
73 return {
74 name,
75 onChange,
76 checked: state.mock[name] ?? false,
77 };
78 }
79
80 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

1DevTools.tsx
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 to
1mocks
. In this example I'm using graphql, but you could easily replace that with whatever REST API you may be using.

1// DevTools.tsx
2import { setupWorker, graphql } from "msw";
3
4export const mockServer = setupWorker();
5
6const mocks = {
7 users: [
8 graphql.query("GetUsers", (req, res, ctx) => {
9 // return fake data
10 }),
11 graphql.query("GetUser", (req, res, ctx) => {
12 // return fake data
13 }),
14 ],
15};

UI

I make a checkbox for every key within

1mocks
. The
1getInputProps
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.

1// DevTools.tsx
2
3function DevTools() {
4 const [mocks, setMocks] = React.useState({});
5
6 function getInputProps(name: string) {
7 function onChange(event: React.ChangeEvent<HTMLInputElement>) {
8 const apiToMock = event.target.name;
9 const shouldMock = event.target.checked;
10
11 setState((prevState) => ({
12 ...prevState,
13 [apiToMock]: shouldMock,
14 }));
15 }
16
17 return {
18 name,
19 onChange,
20 checked: state.mock[name] ?? false,
21 };
22 }
23
24 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.tsx
2export const mockServer = setupWorker();
3
4function 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 );
14
15 React.useEffect(() => {
16 mockServer.start().then(() => {
17 mockServerReady.current = true;
18 });
19
20 return () => {
21 mockServer.resetHandlers();
22 mockServer.stop();
23 };
24 }, []);
25
26 React.useEffect(() => {
27 if (mockServerReady.current) {
28 flushMockServerHandlers();
29 }
30 }, [state.mock]);
31
32 function flushMockServerHandlers() {
33 mockServer.resetHandlers();
34 addHandlersToMockServer(activeMocks);
35 }
36
37 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

1mockServerReady
ref to
1true
. When it unmounts, we reset all the handlers and stop the server.

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
11 React.useEffect(() => {
12 mockServer.start().then(() => {
13 mockServerReady.current = true;
14 });
15
16 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 );
10
11 React.useEffect(() => {
12 mockServer.start().then(() => {
13 mockServerReady.current = true;
14 });
15
16 return () => {
17 mockServer.resetHandlers();
18 mockServer.stop();
19 };
20 }, []);
21
22 React.useEffect(() => {
23 if (mockServerReady.current) {
24 flushMockServerHandlers();
25 }
26 }, [state.mock]);
27}

That's all folks!