💡

(記事作成途中)React×Golang×クリーンアーキテクチャなTodoApp!

2024/03/29に公開

ReactとGolangを用いた開発

開発自体は終了し現在復習のため執筆中です。ごめんなさい🙏

経緯

今までReactとGolangは単体で遊んでいましたが、お二人同士を正式にご紹介したことがありませんでした。🤵‍♂️👰‍♀️ そうですね、クライアントサイドとサーバーサイドが一緒に手を取り合って歩いている様子を見たかったんです。

実は二人はとってもお似合いなんです。Reactはクライアントサイドの振る舞いを優雅に描写し、Golangはサーバーサイドの骨太なロジックを力強く支えてくれます。お互いに相手の長所を認め合い、時には批判し合いながらも、それでも仲睦まじく共に成長していけるんじゃないかと期待しているんですよ。

ただし、お付き合いは長くないので、まだ二人の接し方がうまく分かっていない部分もあります。そこで、教材を使ってデートプランを立ててみることにしました。二人の理想の関係性を学びながら、現場でもよく見られる振る舞いにも注目していきたいと思います。冗談交じりでしたが、このように楽しみながら学習を進めていきたいと思います!

学びたい点!

  • クリーンアーキテクチャでのサーバーサイドの実装方法
  • クライアントサイドとサーバーサイドの接続の方法
  • GolangでのAPI実装方法
  • Reactでのクライアントサイドの実装方法

上記4点に注目しながら学習をしていきたいと思います!

クリーンアーキテクチャとは

クリーンアーキテクチャとは?

クリーンアーキテクチャは、ソフトウェア開発の手法の一つで、アプリケーションの設計を層に分けて整理し、それぞれの層が独立していることを重視します。

クリーンアーキテクチャの核心

クリーンアーキテクチャの核心は、ソフトウェアの構造を整理し、各層が独立して機能するようにすることです。これにより、変更が容易でテストしやすく、かつ拡張可能なシステムを構築できます。

例えば、Reactをフロントエンド層として、Golangをバックエンド層として扱うことで、それぞれの技術の強みを生かしながら、全体のアーキテクチャをクリーンに保つことができます。

クリーンアーキテクチャを実践するには?

クリーンアーキテクチャを実践するためには、具体的な設計原則やパターンを適用する必要があります。たとえば、依存性逆転の原則や、レイヤ間の明確な境界を定義することが重要です。

クリーンアーキテクチャを効果的に実践するには、設計の原則に精通し、各層を適切に分離することが求められます。これにより、各技術のポテンシャルを最大限に活かしながら、一貫性のある開発プロセスを実現することができます。

今回実装するデザイン

.
|--model層
|--db層
|--repository層
|--usecase層
|--controller層
|--router & middleware層
└──client side

各層の役割

モデル層

モデル層は、アプリケーションのデータ構造とビジネスロジックを定義します。

これには、データの形式、データ間の関係、およびデータに対する操作が含まれます。

この層はデータの扱い方を規定し、アプリケーションの他の部分がどのようにデータにアクセスし操作するかの基礎を提供します。

db層

DB層は起動しているデータベース(今回はpostgresql)に接続する役割を果たしています.

repository層

リポジトリ層は実際にデータベースにデータを追加したり、データを取得する役割を果たしています。

usecase層

ユースケース層(またはアプリケーション層)は、アプリケーションのビジネスロジックをカプセル化します。

この層は、アプリケーションの機能を実現するためのワークフローや操作を定義し、外部のリクエストを適切なビジネスロジックに変換します。

この層は、アプリケーションが実行すべき具体的なタスクを表します。

controller層

コントローラ層は、外部のリクエストを受け取り、それをユースケース層またはアプリケーション層の処理に渡す役割を担います。

この層はユーザーのアクションを解釈し、必要なデータを集め、適切なビジネスロジックの実行をコーディネートします。

コントローラは一般に、入力を受け取り、それを処理し、ユーザーへの応答を生成する責任を持ちます。

router & middleware層

ルーターは入ってくるリクエストを適切なコントローラやハンドラにルーティングする機能を提供します。

ミドルウェアは、リクエストとレスポンスの処理過程において実行される一連の関数で、認証、ロギング、エラー処理などの共通タスクを扱います。

この層は、リクエストの処理フローを管理し、必要に応じて追加の処理を実施します。

cliend side (React)

クライアントサイドは、ユーザーが直接対話するインターフェース部分を指します。

Reactを使用したクライアントサイド開発では、ユーザーインターフェースの構築、ユーザーイベントの処理、サーバーサイドとの通信を行います。

Reactはコンポーネントベースのアプローチを提供し、再利用可能なUI部品を通じて効率的なフロントエンド開発を可能にします。

Golang(サーバーサイド)での実装

サーバーサイドのディレクトリ

Reactでの実装

src内の構成

.
├── App.tsx
├── components
│   ├── Auth.tsx
│   ├── TaskItem.tsx
│   └── Todo.tsx
├── hooks
│   ├── useError.ts
│   ├── useMutateAuth.ts
│   ├── useMutateTask.ts
│   └── useQueryTasks.ts
├── index.css
├── index.tsx
├── logo.svg
├── react-app-env.d.ts
├── reportWebVitals.ts
├── setupTests.ts
├── store
│   └── index.ts
└── types
    └── index.ts

types

export type Task = {
  id: number;
  title: string;
  created_at: Date;
  updated_at: Date;
};
export type CsrfToken = {
  csrf_token: string;
};
export type Credential = {
  email: string;
  password: string;
};

この選択範囲では、TypeScriptの型宣言が3つあります。これらは、プログラム内で使用される特定のオブジェクトの形状を定義します。

まず、Task型は、タスクに関する情報を表すオブジェクトの形状を定義します。このオブジェクトは、id(数値)、title(文字列)、created_at(日付)、updated_at(日付)の4つのプロパティを持つことが期待されます。

次に、CsrfToken型は、CSRFトークンを表すオブジェクトの形状を定義します。このオブジェクトは、csrf_token(文字列)という1つのプロパティを持つことが期待されます。

最後に、Credential型は、ユーザーの認証情報を表すオブジェクトの形状を定義します。このオブジェクトは、email(文字列)とpassword(文字列)の2つのプロパティを持つことが期待されます。

これらの型宣言は、プログラム内でのデータの一貫性を保つために重要です。これにより、関数やコンポーネントが期待するデータの形状を明示的に示すことができ、型エラーを防ぐことができます。

store

import { create } from "zustand";

type EditedTask = {
  id: number;
  title: string;
};

type State = {
  editedTask: EditedTask;
  updateEditedTask: (payload: EditedTask) => void;
  resetEditedTask: () => void;
};

const useStore = create<State>((set) => ({
  editedTask: { id: 0, title: "" },
  updateEditedTask: (payload) =>
    set({
      editedTask: payload,
    }),
  resetEditedTask: () => set({ editedTask: { id: 0, title: "" } }),
}));

export default useStore;

この選択範囲では、zustandというライブラリを使用して、アプリケーションの状態管理ストアを作成しています。

まず、EditedTaskという型を定義しています。これは、編集中のタスクを表すオブジェクトの形状を定義しています。このオブジェクトは、id(数値)とtitle(文字列)の2つのプロパティを持つことが期待されます。

次に、Stateという型を定義しています。これは、アプリケーションの状態を表すオブジェクトの形状を定義しています。このオブジェクトは、editedTaskEditedTask型)、updateEditedTask(関数)、resetEditedTask(関数)の3つのプロパティを持つことが期待されます。

create関数を使用して、State型のストアを作成しています。このストアは、初期状態としてeditedTaskを持ち、updateEditedTaskresetEditedTaskという2つのアクションを提供します。updateEditedTaskは、新しいEditedTaskを受け取り、ストアのeditedTaskを更新します。resetEditedTaskは、editedTaskを初期状態にリセットします。

最後に、このストアをデフォルトエクスポートしています。これにより、他のコンポーネントからこのストアをインポートして使用することができます。

index

import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";

const queryClient = new QueryClient({});

const root = ReactDOM.createRoot(
  document.getElementById("root") as HTMLElement
);
root.render(
  <React.StrictMode>
    <QueryClientProvider client={queryClient}>
      <App />
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  </React.StrictMode>
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

このコードは、ReactとTypeScriptを使用したJavaScriptプロジェクトのエントリーポイントです。アプリケーションのルートコンポーネントをレンダリングし、いくつかの重要なツールと設定をセットアップします。

まず、必要なモジュールをインポートします。これには、React自体、アプリケーションのレンダリングに使用するReactDOM、メインのAppコンポーネント、パフォーマンスレポートのためのreportWebVitals関数、そしてReactで非同期データのフェッチ、キャッシュ、更新を行うためのライブラリであるreact-queryに関連するモジュールが含まれます。

新しいQueryClientインスタンスが作成されます。これはreact-queryライブラリの一部で、アプリケーション内のクエリがどのように動作するかを設定するために使用されます。

次に、ReactDOM.createRoot()を使用してルートDOMノードが作成されます。これはReactの新しい並行モードAPIの一部で、更新がどのようにスケジュールされ、レンダリングされるかをよりよく制御することができます。

その後、アプリケーションはこのルートノードにレンダリングされます。React.StrictModeコンポーネントはアプリケーションをラップするために使用され、開発中にアプリケーションの潜在的な問題を強調表示します。

React.StrictMode内では、QueryClientProviderコンポーネントが使用され、queryClientインスタンスがアプリケーションの残りの部分に提供されます。これにより、アプリケーションの任意のコンポーネントがreact-queryの機能を利用することができます。

また、ReactQueryDevtoolsコンポーネントも含まれています。これは、開発中にreact-queryのデバッグツールを提供します。initialIsOpenプロップはfalseに設定されているため、デフォルトではdevtoolsパネルは閉じられています。

最後に、reportWebVitals関数が呼び出されます。この関数は、Create React Appで作成されたプロジェクトに含まれており、Googleがユーザーエクスペリエンスに重要と認定した一連のパフォーマンスメトリクス、ウェブバイタルを測定し、レポートするために使用できます。

App.tsx

import { useEffect } from "react";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import { Auth } from "./components/Auth";
import { Todo } from "./components/Todo";
import axios from "axios";
import { CsrfToken } from "./types";

function App() {
  useEffect(() => {
    axios.defaults.withCredentials = true;
    const getCsrfToken = async () => {
      const { data } = await axios.get<CsrfToken>(
        `${process.env.REACT_APP_API_URL}/csrf`
      );
      axios.defaults.headers.common["X-CSRF-Token"] = data.csrf_token;
    };
    getCsrfToken();
  }, []);
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Auth />} />
        <Route path="/todo" element={<Todo />} />
      </Routes>
    </BrowserRouter>
  );
}

export default App;

このコードは、ReactとTypeScriptを使用したJavaScriptプロジェクトのメインアプリケーションコンポーネントです。このコンポーネントは、アプリケーションのルーティングとCSRFトークンの取得を管理します。

まず、必要なモジュールをインポートします。これには、ReactのuseEffectフック、react-router-domからのルーティング関連のコンポーネント、そしてアプリケーションのAuthTodoコンポーネント、そしてHTTPリクエストを行うためのaxiosライブラリが含まれます。

App関数コンポーネント内では、useEffectフックを使用して、コンポーネントがマウントされたときにCSRFトークンを取得します。これは、サーバーからのクロスサイトリクエストフォージェリ(CSRF)攻撃を防ぐためのセキュリティ対策です。トークンは、サーバーから取得され、その後のすべてのaxiosリクエストのヘッダーに含まれます。

次に、BrowserRouterRoutesコンポーネントを使用して、アプリケーションのルーティングを設定します。/パスにはAuthコンポーネントが、/todoパスにはTodoコンポーネントがレンダリングされます。

Auth.tsx from Components

import { useState, FormEvent } from "react";
import { CheckBadgeIcon, ArrowPathIcon } from "@heroicons/react/24/solid";
import { useMutateAuth } from "../hooks/useMutateAuth";

export const Auth = () => {
  const [email, setEmail] = useState("");
  const [pw, setPw] = useState("");
  const [isLogin, setIsLogin] = useState(true);
  const { loginMutation, registerMutation } = useMutateAuth();

  // 認証処理を行うハンドラ
  const submitAuthHandler = async (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    if (isLogin) {
      loginMutation.mutate({
        email: email,
        password: pw,
      });
    } else {
      await registerMutation
        .mutateAsync({
          email: email,
          password: pw,
        })
        .then(() =>
          loginMutation.mutate({
            email: email,
            password: pw,
          })
        );
    }
  };

  return (
    // ... (中略) ...
  );
};

例文

  1. ログインの場合
// メールアドレスとパスワードを入力
setEmail("user@example.com");
setPw("password123");

// ログインボタンをクリック
submitAuthHandler(event);

この場合、loginMutation.mutate が呼び出され、以下のようなリクエストデータが送信されます。

{
  "email": "user@example.com",
  "password": "password123"
}
  1. 新規登録の場合
// メールアドレスとパスワードを入力
setEmail("newuser@example.com");
setPw("newpassword456");

// 新規登録モードに切り替え
setIsLogin(false);

// 登録ボタンをクリック
submitAuthHandler(event);

この場合、まず registerMutation.mutateAsync が呼び出され、以下のようなリクエストデータが送信されます。

{
  "email": "newuser@example.com",
  "password": "newpassword456"
}

新規登録が成功すると、続けて loginMutation.mutate が呼び出され、以下のようなリクエストデータが送信されます。

{
  "email": "newuser@example.com",
  "password": "newpassword456"
}

このように、ユーザーの入力に応じて、適切なリクエストデータが送信されます。

Todo.tsx from Components

このコードは、Todoアプリケーションのメインコンポーネント Todo を定義しています。以下、コードの解説をします。

import { FormEvent } from "react"; // FormEventをインポート
import { useQueryClient } from "@tanstack/react-query"; // react-queryのuseQueryClientをインポート
import { ArrowRightOnRectangleIcon, ShieldCheckIcon } from "@heroicons/react/24/solid"; // アイコンコンポーネントをインポート
import useStore from "../store"; // stateを管理するストアからuseStoreをインポート
import { useQueryTasks } from "../hooks/useQueryTasks"; // タスク取得用のカスタムフックをインポート
import { useMutateTask } from "../hooks/useMutateTask"; // タスク操作用のカスタムフックをインポート
import { useMutateAuth } from "../hooks/useMutateAuth"; // 認証操作用のカスタムフックをインポート
import { TaskItem } from "./TaskItem"; // TaskItemコンポーネントをインポート

export const Todo = () => {
  const queryClient = useQueryClient(); // キャッシュデータを操作するための関数を取得
  const { editedTask } = useStore(); // ストアからeditedTaskを取得
  const updateTask = useStore((state) => state.updateEditedTask); // ストアからupdateEditedTask関数を取得
  const { data, isLoading } = useQueryTasks(); // タスクデータと読み込み状態を取得
  const { createTaskMutation, updateTaskMutation } = useMutateTask(); // タスク作成・更新用のミューテーション関数を取得
  const { logoutMutation } = useMutateAuth(); // ログアウト用のミューテーション関数を取得

  // タスクの作成・更新を処理するハンドラ
  const submitTaskHandler = (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    if (editedTask.id === 0)
      createTaskMutation.mutate({ title: editedTask.title }); // 新しいタスクを作成
    else {
      updateTaskMutation.mutate(editedTask); // 既存のタスクを更新
    }
  };

  // ログアウトを処理する非同期関数
  const logout = async () => {
    await logoutMutation.mutateAsync(); // ログアウトリクエストを送信
    queryClient.removeQueries(["tasks"]); // タスクデータをキャッシュから削除
  };

  return (
    <div className="flex justify-center items-center flex-col min-h-screen text-gray-600 font-mono">
      <div className="flex items-center my-3">
        <ShieldCheckIcon className="h-8 w-8 mr-3 text-indigo-500 cursor-pointer" />
        <span className="text-center text-3xl font-extrabold">
          Task Manager
        </span>
      </div>
      <ArrowRightOnRectangleIcon
        onClick={logout}
        className="h-6 w-6 my-6 text-blue-500 cursor-pointer"
      />
      <form onSubmit={submitTaskHandler}>
        <input
          className="mb-3 mr-3 px-3 py-2 border border-gray-300"
          placeholder="title ?"
          type="text"
          onChange={(e) => updateTask({ ...editedTask, title: e.target.value })}
          value={editedTask.title || ""}
        />
        <button
          className="disabled:opacity-40 mx-3 py-2 px-3 text-white bg-indigo-600 rounded"
          disabled={!editedTask.title}
        >
          {editedTask.id === 0 ? "Create" : "Update"}
        </button>
      </form>
      {isLoading ? (
        <p>Loading...</p>
      ) : (
        <ul className="my-5">
          {data?.map((task) => (
            <TaskItem key={task.id} id={task.id} title={task.title} />
          ))}
        </ul>
      )}
    </div>
  );
};

例文

このコンポーネントは、以下のような UI を提供します。

  1. ヘッダー部分には "Task Manager" と表示され、シールドのアイコンが表示されます。
  2. 右上にログアウトボタン (矢印のアイコン) が表示されます。
  3. 入力フォームには、タスクのタイトルを入力できます。
  4. 入力フォームの下のボタンは、新しいタスクの作成か既存のタスクの更新かによって、"Create" または "Update" と表示されます。
  5. タスクのリストが表示され、それぞれのタスクには鉛筆アイコンとゴミ箱アイコンが付いています。鉛筆アイコンはタスクの編集、ゴミ箱アイコンはタスクの削除を行います。
  6. タスクデータの読み込み中は "Loading..." と表示されます。

ユーザーがタスクのタイトルを入力してボタンを押すと、submitTaskHandler が呼び出されます。この関数は、editedTaskid が 0 の場合は createTaskMutation.mutate を呼び出して新しいタスクを作成し、そうでない場合は updateTaskMutation.mutate を呼び出して既存のタスクを更新します。

ログアウトボタンをクリックすると、logout 関数が呼び出されます。この関数は、logoutMutation.mutateAsync を呼び出してログアウトリクエストを送信し、その後 queryClient.removeQueries を呼び出してタスクデータをキャッシュから削除します。

このように、このコンポーネントは様々な機能を提供しながら、Todoアプリケーションの中核部分を構成しています。

TaskItem.tsx from Components

このコードは、Reactコンポーネント TaskItem を定義しています。このコンポーネントは、タスクの一覧表示や編集、削除の機能を提供しています。以下、コードの解説をします。

import { FC, memo } from "react"; // Reactのメモ化された関数コンポーネントをインポート
import { PencilIcon, TrashIcon } from "@heroicons/react/24/solid"; // アイコンコンポーネントをインポート
import useStore from "../store"; // stateを管理するストアからuseStoreをインポート
import { Task } from "../types"; // Taskデータ型の定義をインポート
import { useMutateTask } from "../hooks/useMutateTask"; // タスク操作用のカスタムフックをインポート

const TaskItemMemo: FC<Omit<Task, "created_at" | "updated_at">> = ({
  id,
  title,
}) => {
  const updateTask = useStore((state) => state.updateEditedTask); // ストアからupdateEditedTask関数を取得
  const { deleteTaskMutation } = useMutateTask(); // タスク削除用のミューテーション関数を取得

  return (
    <li className="my-3">
      <span className="font-bold">{title}</span>
      <div className="flex float-right ml-20">
        <PencilIcon
          className="h-5 w-5 mx-1 text-blue-500 cursor-pointer"
          onClick={() => {
            updateTask({
              id: id,
              title: title,
            });
          }}
        />
        <TrashIcon
          className="h-5 w-5 text-blue-500 cursor-pointer"
          onClick={() => {
            deleteTaskMutation.mutate(id);
          }}
        />
      </div>
    </li>
  );
};
export const TaskItem = memo(TaskItemMemo);

このコンポーネントは、idtitle をプロパティとして受け取ります。

  • useStore からは updateEditedTask 関数を取得しています。これは、タスクの編集モードを有効にするためのものです。
  • useMutateTask からは deleteTaskMutation 関数を取得しています。これは、タスクを削除するためのものです。

レンダリング結果は以下のようになります。

  • <li> 要素内に title が表示されます。
  • 鉛筆アイコン (PencilIcon) をクリックすると、updateEditedTask 関数が呼び出され、そのタスクの編集モードが有効になります。
  • ゴミ箱アイコン (TrashIcon) をクリックすると、deleteTaskMutation.mutate 関数が呼び出され、そのタスクが削除されます。

最後に、memo 関数でコンポーネントをメモ化しています。これにより、不要なレンダリングを避けてパフォーマンスが向上します。

例文

このコンポーネントは、以下のように使用できます。

const tasks = [
  { id: 1, title: "タスク1" },
  { id: 2, title: "タスク2" },
  { id: 3, title: "タスク3" },
];

return (
  <ul>
    {tasks.map((task) => (
      <TaskItem key={task.id} id={task.id} title={task.title} />
    ))}
  </ul>
);

この場合、以下のようなHTMLが出力されます。

<ul>
  <li class="my-3">
    <span class="font-bold">タスク1</span>
    <div class="flex float-right ml-20">
      <svg
        class="h-5 w-5 mx-1 text-blue-500 cursor-pointer"
        <!-- 鉛筆アイコン -->
      />
      <svg
        class="h-5 w-5 text-blue-500 cursor-pointer"
        <!-- ゴミ箱アイコン -->
      />
    </div>
  </li>
  <!-- 他のタスクも同様にレンダリング -->
</ul>

このように、TaskItem コンポーネントを使用することで、タスクの一覧表示や編集、削除の機能を簡単に実装できます。

useError.ts from Hooks

このコードは、Reactアプリケーションにおけるエラー処理のためのカスタムフック useError を提供しています。以下、コードの解説をします。

# インポート
import axios from "axios"; # HTTPクライアントライブラリ axiosをインポート
import { useNavigate } from "react-router-dom"; # ルーターを操作するための関数 useNavigateをインポート
import { CsrfToken } from "../types"; # CSRF トークンの型定義をインポート
import useStore from "../store"; # stateを管理するストアからuseStoreをインポート

# useError関数
export const useError = () => {
  const navigate = useNavigate(); # ルーター操作用の関数を取得
  const resetEditedTask = useStore((state) => state.resetEditedTask); # ストアからresetEditedTask関数を取得

  # CSRF トークンを取得する非同期関数
  const getCsrfToken = async () => {
    const { data } = await axios.get<CsrfToken>( # GETリクエストを送信してCSRF トークンを取得
      `${process.env.REACT_APP_API_URL}/csrf`
    );
    axios.defaults.headers.common["X-CSRF-TOKEN"] = data.csrf_token; # CSRF トークンをリクエストヘッダーに設定
  };

  # エラーメッセージに応じた処理を行う関数
  const switchErrorHandling = (msg: string) => {
    switch (msg) {
      case "invalid csrf token": # CSRF トークンが無効な場合
        getCsrfToken(); # CSRF トークンを取得
        alert("CSRF token is invalid, please try again"); # アラートを表示
        break;
      case "invalid or expired jwt": # アクセストークンが無効または期限切れの場合
        alert("access token expired, please login"); # アラートを表示
        resetEditedTask(); # 編集中のタスクをリセット
        navigate("/"); # ログインページに遷移
        break;
      case "missing or malformed jwt": # アクセストークンが不正な場合
        alert("access token is not valid, please login"); # アラートを表示
        resetEditedTask(); # 編集中のタスクをリセット
        navigate("/"); # ログインページに遷移
        break;
      case "duplicated key not allowed": # メールアドレスが重複している場合
        alert("email already exist, please use another one"); # アラートを表示
        break;
      case "crypto/bcrypt: hashedPassword is not the hash of the given password": # パスワードが間違っている場合
        alert("password is not correct"); # アラートを表示
        break;
      case "record not found": # メールアドレスが存在しない場合
        alert("email is not correct"); # アラートを表示
        break;
      default: # その他のエラーメッセージの場合
        alert(msg); # エラーメッセージをアラートで表示
    }
  };

  # switchErrorHandling関数をエクスポート
  return { switchErrorHandling };
};

例文

このコードを使って、以下のようにエラー処理を行うことができます。

const { switchErrorHandling } = useError();

// エラーメッセージに応じた処理を行う
switchErrorHandling("invalid csrf token");
switchErrorHandling("invalid or expired jwt");
switchErrorHandling("missing or malformed jwt");
switchErrorHandling("duplicated key not allowed");
switchErrorHandling("crypto/bcrypt: hashedPassword is not the hash of the given password");
switchErrorHandling("record not found");
switchErrorHandling("その他のエラーメッセージ");

useError フックから取得した switchErrorHandling 関数を呼び出すことで、与えられたエラーメッセージに応じた処理が実行されます。

  • invalid csrf token の場合は、新しい CSRF トークンを取得し、アラートを表示します。
  • invalid or expired jwt または missing or malformed jwt の場合は、アクセストークンが無効であることを示すアラートを表示し、編集中のタスクをリセットし、ログインページに遷移します。
  • duplicated key not allowed の場合は、メールアドレスが重複していることを示すアラートを表示します。
  • crypto/bcrypt: hashedPassword is not the hash of the given password の場合は、パスワードが間違っていることを示すアラートを表示します。
  • record not found の場合は、メールアドレスが存在しないことを示すアラートを表示します。
  • その他のエラーメッセージの場合は、そのままアラートで表示します。

このように、エラーメッセージごとに適切な処理を行うことで、ユーザーフレンドリーなエラー処理を実現しています。

useMutateAuth.ts from Hooks

# インポート
import axios from "axios"; # HTTPクライアントライブラリ axiosをインポート
import { useNavigate } from "react-router-dom"; # ルーターを操作するための関数 useNavigateをインポート
import { useMutation } from "@tanstack/react-query"; # react-queryライブラリからuseMutationをインポート
import useStore from "../store"; # stateを管理するストアからuseStoreをインポート
import { Credential } from "../types"; # 認証情報の型定義をインポート
import { useError } from "./useError"; # エラー処理用のカスタムフックuseErrorをインポート

# useMutateAuth関数
export const useMutateAuth = () => {
  const navigate = useNavigate(); # ルーター操作用の関数を取得
  const resetEditedTask = useStore((state) => state.resetEditedTask); # ストアからresetEditedTask関数を取得
  const { switchErrorHandling } = useError(); # エラー処理用のフックからswitchErrorHandling関数を取得

  # ログイン用のミューテーション
  const loginMutation = useMutation(
    async (user: Credential) =>
      await axios.post(`${process.env.REACT_APP_API_URL}/login`, user), # POSTリクエストを送信してログイン
    {
      onSuccess: () => {
        navigate("/todo"); # ログイン成功時にTODOページに遷移
      },
      onError: (err: any) => {
        if (err.response.data.message) {
          switchErrorHandling(err.response.data.message); # エラーメッセージを処理
        } else {
          switchErrorHandling(err.response.data); # エラーデータを処理
        }
      },
    }
  );

  # 新規登録用のミューテーション
  const registerMutation = useMutation(
    async (user: Credential) =>
      await axios.post(`${process.env.REACT_APP_API_URL}/signup`, user), # POSTリクエストを送信して新規登録
    {
      onError: (err: any) => {
        if (err.response.data.message) {
          switchErrorHandling(err.response.data.message); # エラーメッセージを処理
        } else {
          switchErrorHandling(err.response.data); # エラーデータを処理
        }
      },
    }
  );

  # ログアウト用のミューテーション
  const logoutMutation = useMutation(
    async () => await axios.post(`${process.env.REACT_APP_API_URL}/logout`), # POSTリクエストを送信してログアウト
    {
      onSuccess: () => {
        resetEditedTask(); # 編集中のタスクをリセット
        navigate("/"); # ログアウト成功時にホームページに遷移
      },
      onError: (err: any) => {
        if (err.response.data.message) {
          switchErrorHandling(err.response.data.message); # エラーメッセージを処理
        } else {
          switchErrorHandling(err.response.data); # エラーデータを処理
        }
      },
    }
  );

  # ミューテーション関数をエクスポート
  return { loginMutation, registerMutation, logoutMutation };
};

例文

このコードを使って、以下のようにユーザー認証を行うことができます。

  1. ログイン
const { loginMutation } = useMutateAuth();

const handleLogin = (email, password) => {
  loginMutation.mutate({ email, password });
};
  1. 新規登録
const { registerMutation } = useMutateAuth();

const handleRegister = (email, password) => {
  registerMutation.mutate({ email, password });
};
  1. ログアウト
const { logoutMutation } = useMutateAuth();

const handleLogout = () => {
  logoutMutation.mutate();
};

各ミューテーション関数の mutate メソッドを呼び出すことで、対応するリクエストが送信されます。

  • loginMutation の成功時には、/todo にルーターが遷移します。
  • registerMutation には onSuccess が設定されていないため、特別な処理は行われません。
  • logoutMutation の成功時には、編集中のタスクがリセットされ、/ にルーターが遷移します。

エラー発生時には、onError で定義した処理が実行されます。このコードでは、useError フックから取得した switchErrorHandling 関数を使って、エラーメッセージまたはエラーデータを処理しています。

useMutateTask.ts from Hooks

# インポート
import axios from "axios"; # HTTPクライアントライブラリ axiosをインポート
import { useQueryClient, useMutation } from "@tanstack/react-query"; # react-queryライブラリからuseQueryClientとuseMutationをインポート
import { Task } from "../types"; # タスクデータの型定義をインポート
import useStore from "../store"; # stateを管理するストアからuseStoreをインポート
import { useError } from "../hooks/useError"; # エラー処理用のカスタムフックuseErrorをインポート

# useMutateTask関数
export const useMutateTask = () => {
  const queryClient = useQueryClient(); # キャッシュデータを取得・更新するための関数を取得
  const { switchErrorHandling } = useError(); # エラー処理用のフックからswitchErrorHandling関数を取得
  const resetEditedTask = useStore((state) => state.resetEditedTask); # ストアからresetEditedTask関数を取得

  # タスクの作成用のミューテーション
  const createTaskMutation = useMutation(
    # 新しいタスクデータを受け取る関数
    (task: Omit<Task, "id" | "created_at" | "updated_at">) =>
      axios.post<Task>(`${process.env.REACT_APP_API_URL}/tasks`, task), # POSTリクエストを送信して新しいタスクを作成
    {
      # 成功時の処理
      onSuccess: (res) => {
        const previousTasks = queryClient.getQueryData<Task[]>(["tasks"]); # キャッシュからタスクデータを取得
        if (previousTasks) {
          queryClient.setQueryData(["tasks"], [...previousTasks, res.data]); # キャッシュを新しいタスクデータで更新
        }
        resetEditedTask(); # 編集中のタスクをリセット
      },
      # エラー時の処理
      onError: (err: any) => {
        if (err.response.data.message) {
          switchErrorHandling(err.response.data.message); # エラーメッセージを処理
        } else {
          switchErrorHandling(err.response.data); # エラーデータを処理
        }
      },
    }
  );

  # タスクの更新用のミューテーション
  const updateTaskMutation = useMutation(
    # 更新するタスクデータを受け取る関数
    (task: Omit<Task, "created_at" | "updated_at">) =>
      axios.put<Task>(`${process.env.REACT_APP_API_URL}/tasks/${task.id}`, {
        title: task.title, # タイトルのみ更新
      }), # PUTリクエストを送信してタスクを更新
    {
      onSuccess: (res, variables) => {
        const previousTasks = queryClient.getQueryData<Task[]>(["tasks"]); # キャッシュからタスクデータを取得
        if (previousTasks) {
          queryClient.setQueryData<Task[]>( # キャッシュを更新したタスクデータで更新
            ["tasks"],
            previousTasks.map((task) =>
              task.id === variables.id ? res.data : task # 更新したタスクデータと他のデータをマージ
            )
          );
        }
        resetEditedTask(); # 編集中のタスクをリセット
      },
      onError: (err: any) => {
        if (err.response.data.message) {
          switchErrorHandling(err.response.data.message); # エラーメッセージを処理
        } else {
          switchErrorHandling(err.response.data); # エラーデータを処理
        }
      },
    }
  );

  # タスクの削除用のミューテーション
  const deleteTaskMutation = useMutation(
    # 削除するタスクのIDを受け取る関数
    (id: number) =>
      axios.delete(`${process.env.REACT_APP_API_URL}/tasks/${id}`), # DELETEリクエストを送信してタスクを削除
    {
      onSuccess: (_, variables) => {
        const previousTasks = queryClient.getQueryData<Task[]>(["tasks"]); # キャッシュからタスクデータを取得
        if (previousTasks) {
          queryClient.setQueryData<Task[]>( # 削除したタスクを除いたデータでキャッシュを更新
            ["tasks"],
            previousTasks.filter((task) => task.id !== variables)
          );
        }
        resetEditedTask(); # 編集中のタスクをリセット
      },
      onError: (err: any) => {
        if (err.response.data.message) {
          switchErrorHandling(err.response.data.message); # エラーメッセージを処理
        } else {
          switchErrorHandling(err.response.data); # エラーデータを処理
        }
      },
    }
  );

  # ミューテーション関数をエクスポート
  return {
    createTaskMutation, # 作成用のミューテーション関数
    updateTaskMutation, # 更新用のミューテーション関数
    deleteTaskMutation, # 削除用のミューテーション関数
  };
};

例文

このコードを使って、以下のようにタスクデータの操作ができます。

  1. 新しいタスクを作成する
const { createTaskMutation } = useMutateTask();

const handleCreateTask = () => {
  createTaskMutation.mutate({ title: "新しいタスク" });
};
  1. タスクのタイトルを更新する
const { updateTaskMutation } = useMutateTask();

const handleUpdateTask = (task) => {
  updateTaskMutation.mutate({ id: task.id, title: "更新したタイトル" });
};
  1. タスクを削除する
const { deleteTaskMutation } = useMutateTask();

const handleDeleteTask = (taskId) => {
  deleteTaskMutation.mutate(taskId);
};

各ミューテーション関数の mutate メソッドを呼び出すことで、対応するリクエストが送信されます。成功時には onSuccess で定義した処理が実行され、失敗時には onError で定義した処理が実行されます。

useQueryTasks.ts from Hooks

# インポート
import axios from "axios"; # HTTPクライアントライブラリ axiosをインポート
import { useQuery } from "@tanstack/react-query"; # react-queryライブラリからuseQueryをインポート
import { Task } from "../types"; # タスクデータの型定義をインポート
import { useError } from "../hooks/useError"; # エラー処理用のカスタムフックuseErrorをインポート

# useQueryTasks関数
export const useQueryTasks = () => {
  const { switchErrorHandling } = useError(); # エラー処理用のフックからswitchErrorHandling関数を取得

  # タスクデータを取得する非同期関数
  const getTasks = async () => {
    const { data } = await axios.get<Task[]>( # GETリクエストを送信してタスクデータを取得
      `${process.env.REACT_APP_API_URL}/tasks`, # APIのエンドポイント
      { withCredentials: true } # クレデンシャル情報を含める
    );
    return data;
  };

  # useQueryを使ってタスクデータを取得
  return useQuery<Task[], Error>({
    queryKey: ["tasks"], # キャッシュキー
    queryFn: getTasks, # タスクデータを取得する非同期関数
    staleTime: Infinity, # キャッシュの有効期限を無期限に設定
    onError: (err: any) => { # エラー時の処理
      if (err.response.data.message) {
        switchErrorHandling(err.response.data.message); # エラーメッセージを処理
      } else {
        switchErrorHandling(err.response.data); # エラーデータを処理
      }
    },
  });
};

例文

このコードを使って、以下のようにタスクデータを取得できます。

const { data: tasks, isLoading, isError } = useQueryTasks();

if (isLoading) {
  return <div>Loading...</div>;
}

if (isError) {
  return <div>Error!</div>;
}

return (
  <ul>
    {tasks.map((task) => (
      <li key={task.id}>{task.title}</li>
    ))}
  </ul>
);
  1. useQueryTasks フックから useQuery の結果を取得します。
  2. isLoading がtrueの間は、ローディング中の表示をします。
  3. isError がtrueの場合は、エラー表示をします。
  4. tasks が取得できた場合は、タスクのリストを表示します。

useQuery フックは、queryKeyqueryFn を指定することで、自動的にデータのキャッシュ管理を行います。staleTime オプションで、キャッシュの有効期限を設定できます。

エラー発生時には、onError で定義した処理が実行されます。このコードでは、useError フックから取得した switchErrorHandling 関数を使って、エラーメッセージまたはエラーデータを処理しています。

Discussion