😎

StackBlitz上でモックサーバーを立ち上げてTanStack Query(React Query)を素振りしてみる

2024/06/18に公開

こんにちは!CastingONEの大沼です。

始めに

プログラミングの勉強をする際は実際に書いて学ぶのが良いと思っていますが、オンライン上で気軽に書けるとコードの共有&動作確認という観点で尚良いと思っています。僕も簡単なコードの実装はStackBlitzを愛用しています😊
StackBlitzではNodeの実行もできるためバックエンド側の実装を書くこともできるのが魅力です。これによってreact-queryのようなAPIとの通信が必要になるライブラリもモックサーバーを同時に立てることでStackBlitzのみで検証することができます!
そこでこの記事では具体例としてjson-serverreact-queryを使ってStackBlitz上で実装する例を紹介したいと思います。

サンプルコード

先に今回作成したサンプルコードを以下に貼ります。単純に環境が欲しいだけの方やコードの詳細を見たい方はこちらをForkして色々いじって貰えればと思います😄
次のセクションからは具体的にどういう風に環境構築をしたか説明します。


json-serverとreact-queryの環境構築

まずReactの環境についてはテンプレートから選ぶと楽なのでReact TypeScriptのものを選択します。

json-serverのセットアップ

作成されたプロジェクトのターミナルに入り、json-serverをインストールします。

$ npm install -D json-server@^0

続いてモックデータ用のjsonファイルを用意します。とりあえずは以下のように空のdb.jsonを用意するだけで良いです。

$ echo "{}" >> ./db.json

後はpackage.jsonに起動タスクを書いたらモックサーバーの完成です。ここではせっかくモックにしているのでレスポンスに1秒間遅延を挟むようにしています。

package.json
{
  "scripts": {
    "dev:mock": "json-server --watch db.json --delay 1000 --port 6000"
  }
}

このタスクが正常に動くかcurlを使って試してみたいと思います。まずはこのタスクを起動します。StackBlitzのpreview画面が勝手に動いてしまうとは思いますが、そちらは気にしないでください。
この状態で以下のcurlを叩くと実際にデータを作成することができると思います。空だったdb.jsonもPOSTした内容が登録されていると思います。

curlを使ってデータを登録
$ curl -X POST -H "Content-Type: application/json" -d '{"id": 100, "text": "curlから登録"}' http://localhost:6000/todos

このデータをAPIから取得するには以下のcurlで確認できます。

curlを使ってデータを取得
$ curl http://localhost:6000/todos
[
  {
    "id": 100,
    "text": "curlから登録"
  }
]

なお、編集・削除は以下のcurlで実行することができ、モックサーバーとして機能していることが確認できると思います。

curlを使ってデータを編集
$ curl -X PUT -H "Content-Type: application/json" -d '{"id": 100, "text": "curlから編集"}' http://localhost:6000/todos/100
curlを使ってデータを削除
$ curl -X DELETE http://localhost:6000/todos/100

モックサーバーとフロントの開発サーバーを同時に立ち上げる

続いてモックサーバーとフロントの開発サーバーが同時に立ち上がるようにします。ターミナルを分割してそれぞれのタスクを実行しても問題はないですが、一つのタスクで同時に起動できた方が楽だと思うのでその設定を入れます。
タスクを同時に実行するためにここではnpm-run-allを使ったので、それをインストールします。

$ npm install -D npm-run-all

後はpackage.jsonに以下のようなタスクを書いたらnpm run devでモックサーバーとフロントの開発サーバーの両方が立ち上がるようになります。StackBlitzではdevタスクが初回起動時に自動で実行されるようなので、StackBlitzでアクセスしたのと同時にこれらのタスクも立ち上がります。

package.json
{
  "scripts": {
    "dev": "npm-run-all -p dev:mock dev:front",
    "dev:mock": "json-server --watch db.json --delay 1000 --port 6000",
    "dev:front": "vite",
  }
}

react-queryを使ってモックサーバーと通信する

最後にreact-queryをインストールしてモックサーバーと通信できるようにします。

$ npm install @tanstack/react-query
$ npm install -D @tanstack/react-query-devtools

main.tsxを以下のように追加します。

main.tsx
 import React from 'react';
 import ReactDOM from 'react-dom/client';
 import App from './App.tsx';
 import './index.css';

+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { ReactQueryDevtools } from '@tanstack/react-query-devtools';

+const queryClient = new QueryClient();

 ReactDOM.createRoot(document.getElementById('root')!).render(
   <React.StrictMode>
+    <QueryClientProvider client={queryClient}>
       <App />
+      <ReactQueryDevtools />
+    </QueryClientProvider>
   </React.StrictMode>
 );

後はそれぞれ以下のようなリクエストコードを定義してhooksを呼ぶことで通信できるようになります。なお、フロント側でユニークなIDを生成するにはcrypto.randomUUIDを使うのが楽だったためidはstring型で送るように変更しています。詳細のコードは最初に貼ったサンプルコードの方をご参照ください。

TODOの作成
createTodo.ts
import { useMutation, useQueryClient } from '@tanstack/react-query';

import { Todo, getFetchTodosKey } from './fetchTodos';

export const createTodo = (newTodo: Todo) => {
  return fetch('https://localhost:6000/todos', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(newTodo),
  }).then((r) => r.json());
};

export const useMutationCreateTodo = () => {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (newTodo: Todo) => createTodo(newTodo),
    onSuccess: () => {
      queryClient.invalidateQueries({
        queryKey: getFetchTodosKey(),
      });
    },
  });
};
TODOの取得
fetchTodos.ts
import { useQuery } from '@tanstack/react-query';

export type Todo = {
  id: string;
  text: string;
};

export const fetchTodos = () => {
  return fetch('https://localhost:6000/todos').then(
    (r): Promise<Todo[]> => r.json()
  );
};

export const getFetchTodosKey = () => ['todos'];

export const useQueryTodos = () => {
  return useQuery({
    queryKey: getFetchTodosKey(),
    queryFn: () => fetchTodos(),
    refetchInterval: 10 * 1000,
  });
};
TODOの編集
editTodo.ts
import { useMutation, useQueryClient } from '@tanstack/react-query';

import { Todo, getFetchTodosKey } from './fetchTodos';

export const editTodo = (todoId: string, newTodo: Todo) => {
  return fetch(`https://localhost:6000/todos/${todoId}`, {
    method: 'PUT',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(newTodo),
  });
};

export const useMutationEditTodo = () => {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (newTodo: Todo) => editTodo(newTodo.id, newTodo),
    onSuccess: () => {
      queryClient.invalidateQueries({
        queryKey: getFetchTodosKey(),
      });
    },
  });
};
TODOの削除
deleteTodo.ts
import { useMutation, useQueryClient } from '@tanstack/react-query';

import { getFetchTodosKey } from './fetchTodos';

export const deleteTodo = (todoId: string) => {
  return fetch(`https://localhost:6000/todos/${todoId}`, {
    method: 'DELETE',
    headers: {
      'Content-Type': 'application/json',
    },
  }).then((r) => r.json());
};

export const useMutationDeleteTodo = () => {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (todoId: string) => deleteTodo(todoId),
    onSuccess: () => {
      queryClient.invalidateQueries({
        queryKey: getFetchTodosKey(),
      });
    },
  });
};

その他

StackBlitz上でdb.jsonを直接編集するとクラッシュする

npmのタスク上では --watch db.json というオプションが付いているため直接ファイルを編集した場合json-serverが再起動されるのですが、これが上手くいかずタスクが強制終了されてしまいます。エラーを見た感じポートが衝突しているのが原因のようですが、おそらくStackBlitz上ではポートがすぐに解放されないためこのような現象が起きているのかなと思いました🤔 ローカルでは問題なく再起動できていました。
これを何とかするためには自前でjson-serverを立ち上げて再起動ではなくデータだけ更新することで解消できますが、わざわざ書いてまでやるほどなのかなという感じでした。一応以下のようなコードで動くことを確認でき、サンプルコードでもdev-manual:mockというタスク名で起動するようにしているので興味がある方はご参照ください。

自前でjson-serverを管理する
import jsonServer from "json-server";
import fs from "fs";
import path from "path";
import { isEqual } from "lodash-es";

let server: ReturnType<typeof jsonServer.create> | null = null;
let router: jsonServer.JsonServerRouter<object> | null = null;

const startServer = () => {
  server = jsonServer.create();
  router = jsonServer.router("./db.json");
  const middlewares = jsonServer.defaults();

  server.use(middlewares);

  server.use((_req, _res, next) => {
    setTimeout(() => {
      next();
    }, 1000);
  });

  server.use(router);

  server.listen(6000, () => {
    console.log("JSON Server is running");
  });
};

fs.watch(
  // ESModuleで起動している影響で__dirnameは使えないのでダイレクトに相対パスを指定する
  path.resolve("./db.json"),
  (_event, file) => {
    if (file == null || router == null) {
      return;
    }
    try {
      const obj = JSON.parse(fs.readFileSync(file, { encoding: "utf8" }));

      if (!isEqual(obj, router.db.getState())) {
        console.log("db.json directly changed. reload db.json data.");
        router.db.setState(obj);
      }
    } catch (e) {
      console.error(e);
    }
  },
);

startServer();

終わりに

以上がStackBlitz上でモックサーバーを立ち上げてreact-queryを素振りする方法でした。今までAPI通信となると別なサービスを使ってAPIを用意したりしていましたが、無料枠だとずっと立ち上げ続けるわけにもいかずどうしても満足のいくサンプルを作ることができませんでした。それがStackBlitzは全てローカルのリソースを使うため無料枠でも自分用のモックサーバーを立ち上げることができ、オンライン上でAPI通信も含めた検証が非常にやりやすくなりました😄 react-queryなどAPIが絡むコードの検証をしたいときの参考になれれば幸いです。

弊社ではいっしょに働いてくれるエンジニアを募集中です。社員でもフリーランスでも、フルタイムでも短時間の副業でも大歓迎なので、気軽にご連絡ください!

https://www.wantedly.com/projects/836878
https://www.wantedly.com/projects/1130967
https://www.wantedly.com/projects/1244229

Discussion