AIR Design for Marketing 事業部 フロントエンドエンジニアのHashigoです。
私は2022年1月に入社する以前は、主にtext/htmlをレスポンスとして受け取ってコーディングを行うフロントエンド環境で開発を行っていました。
現在、私が参加しているプロジェクトではReactで開発を行なっており、且つCSR(Client Side Rendering)のみのSPAであるため、初期表示の時、React側でGETのHTTPリクエストを行うことが多々あります。その場合、初期表示を行うだけでも「待機」「成功」「失敗」という状態をコンポーネント側で持つ必要があります。
text/htmlを受け取るフロントエンド開発では、初期値はサーバーサイドから既に受け取った状態であることが多く、「待機」=「ブラウザのローディング」で、「失敗」の場合もサーバーサイドから返ってくるhtmlがそもそも違うので、フロント側で考慮することがあまりありませんでした。そして、そういった場合の開発では、フロントエンドを先行で開発していても、初期表示に関しては直書きの値を変数に置き換える作業で終止することが主だったと記憶しています。
React(SPA)では、フロントエンドを先行して開発する場合に、変数に当たる箇所を直書きすると、いざAPIからデータを取得するコードを書いた時に、API通信の状態をフォローできていなくて、APIと繋ぎ込む実装が思ったよりも工数がかかってしまった、ということがありました。
そこで、いろいろな記事で紹介されているMSW(Mock Service Worker)を使用して、API開発前にフロントエンド実装を行ったところ、開発体験が良く、実際のAPIを繋ぎ込む作業が以前より楽に行えました。また、私はAPI開発をしたことが無いので、API開発の触りを学ぶという意味でもとても良い経験でした。
今回は、API通信の状態を考慮できるようにUI開発の下地としてMSWを使ってHTTPリクエストのモックを作成したいと思います。
Mock Service Worker
執筆時点のバージョンはv0.44.2です。
1 MSWでHTTPリクエストのモックを定義
ドキュメントにはセットアップや各APIについて丁寧に解説されていますので、こちらに沿って進めていきます。
今回はREST API & Create React Appを想定したコードを記述しています。
1-1 MSWのインストール
npm install msw --save-dev # or yarn add msw --dev
こちらでpackage.jsonに追加されます。
1-2 モックの定義
モックを定義するためのファイルを格納するディレクトリを作成します。
mkdir src/mocks
次にHTTPリクエストのハンドラーを書くファイルを作成します。
touch src/mocks/handlers.ts
今回は、色の名前とカラーコードのリストを取得するAPI(GET)、新しい色をリストに追加するAPIのモック(POST)を作成します。そのために、レスポンス用のデータを用意します。
// src/mocks/data/colors.json [ { "id": "uuid-1", "name": "Red", "code": "#ef4444" }, { "id": "uuid-2", "name": "Orange", "code": "#fb923c" }, { "id": "uuid-3", "name": "Yellow", "code": "#facc15" }, { "id": "uuid-4", "name": "Green", "code": "#22c55e" } ]
次にhandlers.tsにHTTPリクエストを処理するための関数を書きます。
まずは、先ほどのJSONをimportし、色の一覧を取得するGETリクエストのハンドラーとリゾルバーを作ります。
(別途JSONファイルを用意する必要はなく、直接オブジェクトを書いても大丈夫です。)
// src/mocks/handlers.ts import { rest } from "msw"; import colors from "./data/colors.json"; export const handlers = [ rest.get("/mock/colors-list", (req, res, ctx) => { return res(ctx.status(200), ctx.json(colors)); }), ]
MSWドキュメン上では、rest.get()をhandler、handlerの第二引数のコールバック関数をresolverと呼んでいます。
resolverに渡される3つの引数は、順番に下記のようになっています。
- request(req) → リクエストに関する情報が入ったオブジェクト
- response(res) → モックされたレスポンスのオブジェクトを作成する関数
- context(ctx) → レスポンスの内容を変換する関数群が入ったオブジェクト
続けて、新しい色をリストに追加するPOSTリクエストを作ります。
ついでに、同じカラーコードがあった場合はエラーを返すようにしたいと思います。
// src/mocks/handlers.js import { rest } from "msw"; import colors from "./data/colors.json"; // 追記 type ReqBody = { name: string; code: string; }; type ResBody = { id: string; name: string; code: string; }; export const handlers = [ rest.get("/mock/colors-list", (req, res, ctx) => { return res(ctx.status(200), ctx.json(colors)); }), // 追加 rest.post<ReqBody, ResBody>("/mock/create-color", async (req, res, ctx) => { const { name, code } = await req.json(); if (colors.find((color) => color.code === code)) { // 重複するカラーコードがあった場合はエラー return res( ctx.status(409), ctx.json({ errorMessage: `${code}は既に登録されています。`, }) ); }; return res( ctx.status(201), ctx.json({ id: "uuid-5", name, code, }) ); }), ]
requestとresponseの各bodyの値の型付けと、request bodyの取得方法に悩みましたが、
resolver関数の第二引数をasyncにするなど、下記のようににすることで、無事に取得できるようです。
rest.post<ReqBody, ResBody>("/mock/create-color", async (req, res, ctx) => { const { name, code } = await req.json();
書き方については、下記の記事とMSWトップページのTypeScriptバージョンが参考になりました。
※req.bodyはエディタ上で非推奨と表示され、MSWドキュメントにも記述がありませんでした。
2 MSWで定義したモックAPIを動かす
2-1 MSWを初期化
npx msw init <PUBLIC_DIR> --save # Create React Appの場合はこちら npx msw init public/ --save
<PUBLIC_DIR>
の部分はプロジェクトごとにビルドされて公開されるディレクトリを指定します。
使っているフレームワークやライブラリによる違いは下記で確認できます。
Browser - Getting Started - Mock Service Worker Docs
2-2 Service Workerを起動させるためのファイルを作成
touch src/mocks/browser.ts
続いて、作成したbrowser.tsに下記を記述してリクエストハンドラーを渡してService Workerインスタンスを作成します。
// src/mocks/browser.ts import { setupWorker } from "msw"; import { handlers } from "./handlers"; // This configures a Service Worker with the given request handlers. export const worker = setupWorker(...handlers);
setupWorker関数はリクエストハンドラーを受け取り、作成したService Workerインスタンスを操作するメソッドを返してくれるそうです。
setupWorker() - Api - Mock Service Worker Docs
2-3 Service Workerを起動
It's not recommended to include Mock Service Worker in production. Doing so may lead to a distorted experience for your users.
https://mswjs.io/docs/getting-started/integrate/browser#start-worker
上記注意書きのように、production環境では動作させないように先ほどのworkerインスタンスをimportして、Reactのindex.tsでService Workerをアクティブにします。
import React from "react"; import ReactDOM from "react-dom/client"; import App from "./App"; if (process.env.NODE_ENV === "development") { const { worker } = require("./mocks/browser") worker.start() } const root = ReactDOM.createRoot( document.getElementById("root") as HTMLElement ); root.render( <React.StrictMode> <App /> </React.StrictMode> );
ここでようやく、Create React Appを起動します。
npm start # or yarn start
localhostが立ち上がったら、ブラウザのdevtoolでコンソールを確認します。
こちらが表示されていれば、無事に立ち上げ完了です 🎉
ちなみに、yarn buildなどプロダクションビルドすると、上記は表示されず、Service Workerが有効では無いことが分かります。
3 UIからHTTPリクエストをモックしてみる
3-1 まずは、GETリクエストで一覧を取得してみる
下記のようなコードでMSWで定義したAPIにfetchしてみます。
const [colors, setColors] = useState([]); useEffect(() => { getColors(); }, []); const getColors = async () => { try { const response = await fetch("/mock/colors-list"); if (!response.ok) { throw new Error("色の一覧取得に失敗しました"); } const colors = await response.json(); setColors(colors); } catch (error) { if (error instanceof Error) { console.error(error.message); } } };
すると、ブラウザdevtoolのネットワークを見ると、画像のようにちゃんとレスポンスが返ってきています。
3-2 POSTリクエストで新しいデータを追加してみる
下記のコードでformからPOSTメソッドでfetchしてみます。
const [colors, setColors] = useState([]); const handleSubmit = async (e: React.SyntheticEvent) => { e.preventDefault(); // formから取得したinputのvalue const target = e.target as typeof e.target & { name: { value: string }; code: { value: string }; }; const name = target.name.value; const code = target.code.value; try { const response = await fetch("/mock/create-color", { method: "POST", body: JSON.stringify({ name, code, }), }); if (!response.ok) { const error = await response.json(); throw new Error(error.errorMessage); } const newColor = await response.json(); setColors([...colors, newColor]); } catch (error) { if (error instanceof Error) { console.error(error.message); } } }; return ( <div className="App"> {colors.length !== 0 && ( <ul> {colors.map((color, index) => ( <li> <dl> <dt>{color.name}</dt> <dd>{color.code}</dd> </dl> </li> ))} </ul> )} <form onSubmit={handleSubmit}> <input type="text" name="name" id="color-name" /> <input type="color" name="code" id="color-code" /> <button type="submit">追加</button> </form> </div> );
input type=”name”
とinput type=”color”
に値を入れてsubmitすると、画像のようにsubmitしたデータが追加され、レスポンスが返ってきました。
さらに、存在するカラーコード(#22c55e)を入れてPOSTリクエストしてみます。
定義した通り、403エラーでエラーメッセージをレスポンスとして返すことができました。
HTTPリクエストがモックできた後は
MSWでモックAPIを作れたので、「待機」「成功」「失敗」を意識してコードを書くことができるようになりました。
下記のようにstateによってreturnするJSXを出し分けることで、各状態のUIを実装することができます。
const [loading, setLoading] = useState<boolean>(true); const [error, setError] = useState<string>(""); if (loading) { return <div>Loading...</div> } if (error) { return <p>{error}</p> } return <div>成功した時のDOM</div>