🐷

React(Typescript)のカスタムフックとExpress(Typescript)のrouteでリファクタリング

2023/08/07に公開

記事の対象者

ここで取り上げているコードはReact + Express + MySQLの記事で記述したコードをもとにReactではカスタムフック化、Expressはrouterを使用することでコードの可読性を高めていく記事となっています。以前の記事のコードでなくてもどのようにカスタムフックにするのか、Expressでrouterを使用するにはという部分で見ていただいたり、自分が書いたコードをリファクタリングしてみたりの参考にしていただければと思います。

Expressのrouterを使用しファイル分割でコードの可読性up

ファイル分割する前のコードです。

index.ts
import express, { Application, Request, Response } from "express";
import cors from "cors";
import { uid } from "uid";
import mysql from "mysql2";

const connection = mysql.createConnection({
  host: "localhost",
  user: "root",
  password: "HiKaRu!4215",
  database: "todos_test",
});

const app: Application = express();
const PORT = 3000;

app.use(cors({ origin: "http://localhost:5173" }));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

app.get("/", (req: Request, res: Response) => {
  console.log("getリクエストを受け付けました。");
  const sql = "SELECT * FROM todo";
  connection.query(sql, (error, result) => {
    if (error) {
      res.status(500).json({ message: error.message });
    } else {
      res.status(200).json({ todos: result });
    }
  });
});

app.post("/add", (req: Request, res: Response) => {
  console.log("postリクエストを受け付けました。");
  const { todo } = req.body.data;
  const uidValue = uid();
  const sql = `INSERT INTO todo VALUES ("${uidValue}", "${todo}")`;
  connection.query(sql, (error, result) => {
    if (error) {
      console.log(error);
      return res.status(500).json({ message: "Failed to add todo" });
    }
    return res.status(200).json({ id: uidValue, todo });
  });
});

app.delete("/delete", (req: Request, res: Response) => {
  console.log("deleteリクエストを受け付けました。");
  console.log(req.body.id);
  const id = req.body.id;
  const sql = `DELETE FROM todo WHERE id = "${id}"`;
  connection.query(sql, (error) => {
    if (error) {
      res.status(500).json({ message: error.message });
    } else {
      res.status(200).json({ message: "success" });
    }
  });
});

app.put("/update", (req: Request, res: Response) => {
  console.log("putリクエストを受け付けました。");
  console.log(req.body.data);
  const { id, todo } = req.body.data;
  const sql = `UPDATE todo SET todo="${todo}" WHERE id="${id}"`;
  connection.query(sql, (error) => {
    if (error) {
      res.status(500).json({ message: error.message });
    } else {
      res.status(200).json({ id, todo });
    }
  });
});

try {
  app.listen(PORT, () => {
    console.log(`server running at://localhost:${PORT}`);
  });
} catch (e) {
  if (e instanceof Error) {
    console.error(e.message);
  }
}

最終的なフォルダ構成(Express)

  • 📁backend
    • 📁node_modules
    • 📁db
      • 📄connection.ts
    • 📁router
      • 📄deleteTodoRouter.ts
      • 📄getTodoRouter.ts
      • 📄postTodoRouter.ts
      • 📄updateTodoRouter.ts
    • 📄index.ts
    • 📄.env
    • 📄.gitignore
    • 📄package-look.json
    • 📄package.json
    • 📄tsconfig.json

DBのconnectionをモジュール化

1ファイルで管理していたので、DBのコネクションのコードも同じファイルで管理するので問題はなかったのですが、ファイル分割するとそれぞれでDBと接続する部分があるので、全部に記述していると冗長になってしまいます。そうならないように、モジュール化してどのファイルからでも呼び出せるようにして行きましょう。ついでと言っては何ですが、ユーザー名やパスワードなど秘匿するために環境変数にして呼び出していきましょう。
※gitなどにpushするときは.gitignoreファイルに.envを設定しましょう

MYSQL_USER=ユーザー名
MYSQL_PASSWORD=パスワード

環境変数を使用するときにはライブラリをインストールする必要があります。

npm install dotenv

exportでどこからでも呼び出して使えるように実装しています。
使用方法としては使用したいところでimportして使用します。

db/connection.ts
import mysql from "mysql2";
import dotenv from "dotenv";
dotenv.config()

export const connection = mysql.createConnection({
    host: "localhost",
    user: process.env.MYSQL_USER,
    password: process.env.MYSQL_PASSWORD,
    database: "データベース名",
  });

GETメソッド

router/getTodoRouter.ts
import { Router, Request, Response } from "express";
import { connection } from "../db/connection";

export const getTodoRouter = () => {
  const router = Router(); 

  // todoの一覧を返却するAPI
  router.get("/", (req: Request, res: Response) => {
    const sql = "SELECT * FROM todos";
    connection.query(sql, (error, result) => {
      if (error) {
        res.status(500).json({ message: error.message });
      } else {
        res.status(200).json(result);
      }
    });
  });

  return router
};

上記実装で、モジュール化にしたので、起点となるindex.tsで呼び出して行きましょう。

index.ts
import express, { Application, Request, Response } from "express";
import cors from "cors";
import { uid } from "uid";
import { connection } from "./db/connection"; 

import { getTodoRouter } from "./router/getTodoRouter";
// モジュール化したGETメソッドを呼び出しています。

const app: Application = express();
const PORT = 3000;

app.use(cors({origin: "http://localhost:5173"}));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

app.use("/", getTodoRouter());  
// URLが"http://localhost:3000"の場合にモジュール化したgetTodoRouterを実行しています。

// 以下はpostメソッド等の処理になります。

上記の要領でほかのメソッドの場合もファイルを分割していきます。

POSTメソッド

router/postTodoRouter.ts
import { Router, Request, Response } from "express";
import { connection } from "../db/connection";
import { uid } from "uid";

export const postTodoRouter = () => {
  const router = Router();
  router.post("/", (req: Request, res: Response) => {
    // パスには何も設定しない。起点となるindex.tsで/addを設定しているため
    console.log("postリクエストを受け付けました。");
    const { todo } = req.body.data;
    const uidValue = uid();
    const sql = `INSERT INTO todo VALUES ("${uidValue}", "${todo}")`;
    connection.query(sql, (error, result) => {
      if (error) {
        console.log(error);
        return res.status(500).json({ message: "Failed to add todo" });
      }
      return res.status(200).json({ id: uidValue, todo });
    });
  });
  return router;
};

getメソッド同様にindex.tsファイルで呼び出していきます。その時に、パスをaddで設定しているので、ファイルを分割したpostTodoRouterのパスには何も設定しないようにしましょう。

index.ts
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

app.use("/", getTodoRouter()); 
app.use("/add", postTodoRouter()); 
// URLが"http://localhost:3000/add"の場合にモジュール化したpostTodoRouterを実行しています。

DELETEメソッド

router/deleteTodoRouter.ts
import { Router, Request, Response } from "express";
import { connection } from "../db/connection";

export const deleteTodoRouter = () => {
  const router = Router();
  router.delete("/", (req: Request, res: Response) => {
    // パスには何も設定しない。起点となるindex.tsで/deleteを設定しているため
    console.log("deleteリクエストを受け付けました。");
    console.log(req.body.id);
    const id = req.body.id;
    const sql = `DELETE FROM todo WHERE id = "${id}"`;
    connection.query(sql, (error) => {
      if (error) {
        res.status(500).json({ message: error.message });
      } else {
        res.status(200).json({ message: "success" });
      }
    });
  });
  return router;
};

getメソッド同様にindex.tsファイルで呼び出していきます。その時に、パスをdeleteで設定しているので、ファイルを分割したdeleteTodoRouterのパスには何も設定しないようにしましょう。

index.ts
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

app.use("/", getTodoRouter()); 
app.use("/add", postTodoRouter());
app.use("/delete", deleteTodoRouter());
// URLが"http://localhost:3000/delete"の場合にモジュール化したdeleteTodoRouterを実行しています。

PUTメソッド

getメソッド同様にindex.tsファイルで呼び出していきます。その時に、パスをdeleteで設定しているので、ファイルを分割したdeleteTodoRouterのパスには何も設定しないようにしましょう。

router/putTodoRouter.ts
import { Router, Request, Response } from "express";
import { connection } from "../db/connection";

export const putTodoRouter = () => {
  const router = Router();
  router.put("/", (req: Request, res: Response) => {
    // パスには何も設定しない。起点となるindex.tsで/updateを設定しているため
    console.log("putリクエストを受け付けました。");
    console.log(req.body.data);
    const { id, todo } = req.body.data;
    const sql = `UPDATE todo SET todo="${todo}" WHERE id="${id}"`;
    connection.query(sql, (error) => {
      if (error) {
        res.status(500).json({ message: error.message });
      } else {
        res.status(200).json({ id, todo });
      }
    });
  });
  return router;
};
index.ts
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

app.use("/", getTodoRouter()); 
app.use("/add", postTodoRouter());
app.use("/delete", deleteTodoRouter());
app.use("/update", updateTodoRouter());
// URLが"http://localhost:3000/update"の場合にモジュール化したupdateTodoRouterを実行しています。

バックエンドの最終的なindex.ts

index.ts
import express, { Application } from "express";
import cors from "cors";

import { getTodoRouter } from "./router/getTodoRouter";
import { postTodoRouter } from "./router/postTodoRouter";
import { deleteTodoRouter } from "./router/deleteTodoRouter";
import { putTodoRouter } from "./router/putTodoRouter";

const app: Application = express();
const PORT = 3000;

app.use(cors({ origin: "http://localhost:5173" }));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

app.use("/", getTodoRouter());
app.use("/add", postTodoRouter());
app.use("/delete", deleteTodoRouter());
app.use("/update", putTodoRouter());

try {
  app.listen(PORT, () => {
    console.log(`server running at://localhost:${PORT}`);
  });
} catch (e) {
  if (e instanceof Error) {
    console.error(e.message);
  }
}

ファイル分割したことにより、全部まとまっていたindex.tsファイルの時よりもだいぶ見やすくなったのではないでしょうか。
次はフロント側の実装を行っていきましょう。

Reactのコードをカスタムフック化

ファイル分割前のApp.tsxファイルのコードです。

App.tsx
import { useEffect, useState } from "react";
import "./App.css";
import axios from "axios";
import { useForm } from "react-hook-form";

type TodoTypes = {
  id: string;
  todo: string;
};

type AddTodoType = {
  todo: string;
  editTodoName: string;
};

function App() {
  const { register, handleSubmit, reset } = useForm<AddTodoType>();
  const [todos, setTodos] = useState<TodoTypes[]>([]);
  const [isEdit, setIsEdit] = useState({ id: "", todo: "" });

  const addTodo = async (event: AddTodoType) => {
    const { todo } = event;
    console.log(todo);
    await axios
      .post("http://localhost:3000/add", {
        data: {
          todo,
        },
      })
      .then((response) => {
        console.log(response.data);
        const todo = response.data;
        setTodos((preTodos) => [todo, ...preTodos]);
      })
      .catch((error) => {
        console.log(error);
      });
  };

  const deleteTodo = async (id: string) => {
    console.log(id);

    await axios
      .delete("http://localhost:3000/delete", {
        data: {
          id,
        },
      })
      .then((response) => {
        console.log(response);
        const newTodos = todos.filter((todo) => todo.id !== id);
        setTodos(newTodos);
      })
      .catch((e) => {
        console.log(e.message);
        setTodos(todos);
      });
  };

  const editTodo = async ({ editTodoName }: AddTodoType) => {
    await axios
      .put("http://localhost:3000/update", {
        data: {
          id: isEdit.id,
          todo: editTodoName,
        },
      })
      .then((response) => {
        console.log(response.data);
        const newTodos = todos.map((todo) => {
          return todo.id === response.data.id ? response.data : todo;
        });
        setIsEdit({ id: "", todo: "" });
        setTodos(newTodos);
        reset();
      })
      .catch((e) => {
        console.log(e.message);
      });
  };

  useEffect(() => {
    axios.get("http://localhost:3000").then((response) => {
      console.log(response.data.todos);
      const { todos } = response.data;
      setTodos(todos);
    });
  }, []);

  return (
    <>
      <form onSubmit={handleSubmit(addTodo)}>
        <input {...register("todo")} type="text" />
        <button type="submit">add</button>
      </form>
      {todos.map((todo) => (
        <div key={todo.id} style={{ display: "flex" }}>
          {isEdit.id === todo.id ? (
            <form onSubmit={handleSubmit(editTodo)}>
              <input {...register("editTodoName")} type="text" />
              <button>send</button>
            </form>
          ) : (
            <>
              <p>{todo.todo}</p>
              <button
                onClick={() => setIsEdit({ id: todo.id, todo: todo.todo })}
              >
                edit
              </button>
            </>
          )}

          <button onClick={() => deleteTodo(todo.id)}>delete</button>
        </div>
      ))}
    </>
  );
}

export default App;

最終的なフォルダ構成(React)

  • 📁frontend
    • 📁node_modules
    • 📁public
    • 📁src
      • 📁api
        • 📄useTodoAPI.tsx
      • 📁types
         - 📄api.d.ts
    • 📄App.tsx
      全部記載すると長くなってしまうので、ここまでにとどめておきますが、今回作成したものはsrcの下にapiフォルダを作成し、apiフォルダの部分とtypesフォルダの部分になります。カスタムフックとして使用する場合はuseから始まるファイル名にするのがルールとなっています。

カスタムフックを使用してファイル分割

まずはApp.tsxファイルに記述されている、APIと連携している部分をuseTodoAPI.tsxファイルにごっそりコピペしていきましょう。

App.tsx
import { useEffect, useState } from "react";
import "./App.css";
import axios from "axios";
import { useForm } from "react-hook-form";

type TodoTypes = {
  id: string;
  todo: string;
};

type AddTodoType = {
  todo: string;
  editTodoName: string;
};

function App() {
  const { register, handleSubmit, reset } = useForm<AddTodoType>();
  const [todos, setTodos] = useState<TodoTypes[]>([]);
  const [isEdit, setIsEdit] = useState({ id: "", todo: "" });

  return (
    <>
      <form onSubmit={handleSubmit(addTodo)}>
        <input {...register("todo")} type="text" />
        <button type="submit">add</button>
      </form>
      {todos.map((todo) => (
        <div key={todo.id} style={{ display: "flex" }}>
          {isEdit.id === todo.id ? (
            <form onSubmit={handleSubmit(editTodo)}>
              <input {...register("editTodoName")} type="text" />
              <button>send</button>
            </form>
          ) : (
            <>
              <p>{todo.todo}</p>
              <button
                onClick={() => setIsEdit({ id: todo.id, todo: todo.todo })}
              >
                edit
              </button>
            </>
          )}

          <button onClick={() => deleteTodo(todo.id)}>delete</button>
        </div>
      ))}
    </>
  );
}

export default App;
useTodoAPI.tsx
export default function useTodoAPI() {
  const addTodo = async (event: AddTodoType) => {
    const { todo } = event;
    console.log(todo);
    await axios
      .post("http://localhost:3000/add", {
        data: {
          todo,
        },
      })
      .then((response) => {
        console.log(response.data);
        const todo = response.data;
        setTodos((preTodos) => [todo, ...preTodos]);
      })
      .catch((error) => {
        console.log(error);
      });
  };

  const deleteTodo = async (id: string) => {
    console.log(id);

    await axios
      .delete("http://localhost:3000/delete", {
        data: {
          id,
        },
      })
      .then((response) => {
        console.log(response);
        const newTodos = todos.filter((todo) => todo.id !== id);
        setTodos(newTodos);
      })
      .catch((e) => {
        console.log(e.message);
        setTodos(todos);
      });
  };

  const editTodo = async ({ editTodoName }: AddTodoType) => {
    await axios
      .put("http://localhost:3000/update", {
        data: {
          id: isEdit.id,
          todo: editTodoName,
        },
      })
      .then((response) => {
        console.log(response.data);
        const newTodos = todos.map((todo) => {
          return todo.id === response.data.id ? response.data : todo;
        });
        setIsEdit({ id: "", todo: "" });
        setTodos(newTodos);
        reset();
      })
      .catch((e) => {
        console.log(e.message);
      });
  };

  useEffect(() => {
    axios.get("http://localhost:3000").then((response) => {
      console.log(response.data.todos);
      const { todos } = response.data;
      setTodos(todos);
    });
  }, []);
}

現状いろいろなところで赤線が出ていたり、エラーとなっている部分があるかと思いますがそれらを少しずつ解消していきましょう。

まずはApp.tsxファイルで出ているエラーから修正していきます。
エラーが出ている個所は3点です。
1, addTodo
2, editTodo
3, deleteTodo
上記3点の関数がないことでエラーが出ています。

App.tsx
import { useEffect, useState } from "react";
import "./App.css";
import axios from "axios";
import { useForm } from "react-hook-form";
+ import useTodoAPI from "./api/useTodoAPI";

type TodoTypes = {
  id: string;
  todo: string;
};

type AddTodoType = {
  todo: string;
  editTodoName: string;
};

function App() {
  const { register, handleSubmit, reset } = useForm<AddTodoType>();
  const [todos, setTodos] = useState<TodoTypes[]>([]);
  const [isEdit, setIsEdit] = useState({ id: "", todo: "" });

+  const {addTodo, editTodo, deleteTodo} = useTodoAPI();

  return (

上記実装したことで、return内でのエラーではなく

const {addTodo, editTodo, deleteTodo} = useTodoAPI();

この行に赤線やエラーが集中したと思います。
現在のエラーはuseTodoAPI.tsxにてreturnされている関数がないためにエラーとなっています。

続いてuseTodoAPI.tsxファイルを修正していきます。
まずは、App.tsxで呼び出している3つの関数をreturnで返すところから行っていきましょう。

useTodoAPI.tsx
  return {
    addTodo,
    deleteTodo,
    editTodo
  }

上記コードを記述したら、App.tsxファイルでのエラーはなくなったかと思います。

addTodoの修正

続いてaddTodoの部分で出ているエラーを解消していきましょう。
現在addTodoで出ているエラーの一覧です。
・AddTodoType → 型宣言が見つからない
・axios → ライブラリがimportされていない
・response → 型が設定されていない
・setTodos → 宣言が見つからない
・preTodos → 型が設定されていない
・error → 型が設定されていない
以上が赤線が出ている部分のエラーになります。

AddTodoTypeのエラー解消方法
いくつかの方法があるのですが、App.tsxのuseFormの部分でも使用しているので、1か所に記載して使いまわせるようにしていきましょう。
srcフォルダの下にtypesフォルダを作成します。フォルダ名はなんでもいいのですが、<フォルダ名>.d.tsという名前で作成しましょう。.dを使用することで、そのフォルダに記載されている型をexportや使用するファイルでimportをしなくても使えるようになります。

app.tsxで記載していたAddTodoTypeを私はapi.d.tsファイルにコピペしていきます。

api.d.ts
type AddTodoType = {
  todo: string;
  editTodoName: string;
};

これでAddTodoTypeのエラーは解消されたかと思います。

続いてaxiosのエラーを解消していきましょう。とはいえこれはimportすれば解消するので特に説明はいらないかなと思います。

useTodoAPI.tsx
import axios from "axios";

useTodoAPI.tsxに追加しましょう。

ここで気づいたかと思われますが、axiosのimport文を記述したら、responseやerrorの部分のエラーも消えたかと思います。
ただresponseやerrorの上にカーソルを置いてみると
axiosの型について
写真のようになっているのではないでしょうか。

のようになっており型宣言がされていないことを表しています。
このままでも支障があるわけではないですが、型宣言をしておくのがベターです。
※と言いながらこの元になっているコードでは型定義をしておくのを忘れておりました。すみません。
そしてこの部分はほかの関数の部分でも型宣言をしていくので一番最後にどのように行うか記述します。

setTodoのエラーを解消していきましょう。

App.tsx
function App() {
  const { register, handleSubmit, reset } = useForm<AddTodoType>();
- const [todos, setTodos] = useState<TodoTypes[]>([]);
  const [isEdit, setIsEdit] = useState({ id: "", todo: "" });
useTodoAPI.tsx
export default function useTodoAPI() {
+ const [todos, setTodos] = useState<TodoTypes[]>([]);

  const addTodo = async (event: AddTodoType) => {

まずはApp.tsxに記載しているこのステートをuseTodoAPI.tsxに移しましょう。
useStateのimportとTodoTypesの宣言をしていきましょう。

useTodoAPI.tsx
import axios from "axios";
+ import { useState } from "react";

+ type TodoTypes = {
+   id: string;
+   todo: string;
+ };

export default function useTodoAPI() {
  const [todos, setTodos] = useState<TodoTypes[]>([]);

今度はApp.tsxでエラーが出ていると思いますが、これもaddTodoでやったときみたいにuseTodoAPI.tsxでreturnで返すのと、App.tsxで呼び出しを行っていきましょう。

App.tsx
  const {todos, addTodo, editTodo, deleteTodo} = useTodoAPI()
useTodoAPI.tsx
  return {
 +  todos,
    addTodo,
    deleteTodo,
    editTodo,
  };

deleteTodo

特にエラーは出ていないはずです。最後の型定義の部分が残っているぐらいかと思います。

editTodo

エラー個所一覧
・isEdit → 宣言がない
・setIsEdit → 宣言がない
・reset() → 宣言がない
3点の場所でエラーが出ているかと思います。

isEditとsetIsEditはApp.tsxで記述しているステートをそのままもってきましょう。

App.tsx
  const { register, handleSubmit, reset } = useForm<AddTodoType>();

- const [isEdit, setIsEdit] = useState({ id: "", todo: "" });

  const {todos,addTodo, editTodo, deleteTodo} = useTodoAPI()
useTodoAPI.tsx
export default function useTodoAPI() {
  const [todos, setTodos] = useState<TodoTypes[]>([]);
+ const [isEdit, setIsEdit] = useState({ id: "", todo: "" });

ステートを移動させるとApp.tsxでエラーが出ているかと思います。
isEditについては、App.tsxで呼び出しと、useTodoAPI.tsxでreturn文に追加をしてあげましょう。

App.tsx
  const { todos, isEdit, addTodo, editTodo, deleteTodo } = useTodoAPI();
useTodoAPI.tsx
  return {
    todos,
+   isEdit,
    addTodo,
    deleteTodo,
    editTodo,
  };

setIsEditについてはeditのボタンをクリックしたときにidとtodoをステートに入れている処理になります。この部分の処理をuseTodoAPIに移していきましょう。

useTodoAPI.tsx
+ const selectEditTodo = (todo: TodoTypes) => {
+   setIsEdit({ id: todo.id, todo: todo.todo });
+ };
  
  return {
    todos,
    isEdit,
 +  selectEditTodo,
    addTodo,
    deleteTodo,
    editTodo,
  };

reset()のエラーを解消していきましょう。
App.tsxで呼び出しているresetを使用しないとreset()が呼び出されないので、呼び出し元の関数useTodoAPIに引数として渡してあげましょう。

app.tsx
const { todos, isEdit, selectEditTodo, addTodo, editTodo, deleteTodo } = useTodoAPI(reset);
useTodoAPI.tsx
import { UseFormReset } from "react-hook-form";

export default function useTodoAPI(reset: UseFormReset<AddTodoType>) {

型がわからない場合はApp.tsxのresetの部分にカーソルを合わせれば型が出てくるので調べなくても問題なく型を設定できます。
resetの型について

触れてこなかったのですが、useFeectのエラーはimportすれば問題がないので、ここで軽く触れておきます。

これでエラーはなくなったのではないでしょうか。
一度にカスタムフックにしたので、途中動作を確認するところがなかったと思います。すみません。ただこれでエラーが解消されたので、実際に動作するのか確認して、エラーが出ている部分があれば再度確認して解消してみてください。

Axiosのresponseとerrorの型宣言

最後にこちらにとりかかりましょう。取り合えず動けば問題ないのであればここは必要ないかと思いますが、Typescriptを使用しているので、型宣言を行ってより安全にしていきましょう。

共通部分

useTodoAPI.tsx
import axios, { AxiosResponse, AxiosError } from "axios";

addTodoのresponseとerrorの型宣言

  const addTodo = async (event: AddTodoType) => {
    const { todo } = event;
    console.log(todo);
    await axios
      .post("http://localhost:3000/add", {
        data: {
          todo,
        },
      })
      .then((response: AxiosResponse<TodoTypes>) => {
        console.log(response.data);
        const todo = response.data;
        setTodos((preTodos) => [todo, ...preTodos]);
      })
      .catch((error: AxiosError) => {
        console.log(error);
      });
  };

注目するべきところはここかと思います。

.then((response: AxiosResponse<TodoTypes>) => {

この処理は正常にAPIの通信が終わりバックエンドからのレスポンスを受け取っているところです。
addTodoのレスポンスとしての想定はこちらです。

{
  id: string;
  todo: string;
};

この部分は
const todo = response.data;
todoの部分にどのようなデータが入ってくるかを定義しています。
AxiosResponseを使用して型宣言をする前はany型になっています。
addTodoのresponseのdata型(型定義無)
が型を宣言したことで型の定義がされていることがうかがえます。
addTodoのresponseのdata型(型定義有)

deleteTodoのresponseとerrorの型宣言

deleteではそもそもresponseは受け取っても処理の内容として使用していない(console.log()は使用しているが実際の処理とは関係ない)ので、responseの部分をなくしましょう。

useTodoAPI.tsx
  const deleteTodo = async (id: string) => {
    console.log(id);

    await axios
      .delete("http://localhost:3000/delete", {
        data: {
          id,
        },
      })
      .then(() => {
        const newTodos = todos.filter((todo) => todo.id !== id);
        setTodos(newTodos);
      })
      .catch((e: AxiosError) => {
        console.log(e.message);
        setTodos(todos);
      });
  };

editTodoのresponseとerrorの型宣言

addTodoと同様に処理を記述すれば問題ありません。

  const editTodo = async ({ editTodoName }: AddTodoType) => {
    await axios
      .put("http://localhost:3000/update", {
        data: {
          id: isEdit.id,
          todo: editTodoName,
        },
      })
      .then((response: AxiosResponse<TodoTypes>) => {
        console.log(response.data);
        const newTodos = todos.map((todo) => {
          return todo.id === response.data.id ? response.data : todo;
        });
        setIsEdit({ id: "", todo: "" });
        setTodos(newTodos);
        reset();
      })
      .catch((e: AxiosError) => {
        console.log(e.message);
      });
  };

useEffectの内のGETする際のresponseの型定義

ここで注意する点はGETメソッドでTodoの一覧をとってくるときにtodosとなっていることです。
これまでは単体のみの追加や削除、更新でしたがGETしてくるときは複数あるのでtodosとしていました。
なのでどのようなデータ階層になるかというと、

{
  todos: {
    id: string;
    todo: string
  }
}

このようなデータを受け取ります。
なので、ここで新しく型宣言をしてあげます。

useTodoAPI.tsx
type TodoTypes = {
  id: string;
  todo: string;
};

+ type GetTodoTypes = {
+   todos: TodoTypes[];
+ };

もともとあったTodoTypesを利用して、GetTodoTypesを宣言しています。
それではこれを使って型宣言していきましょう。

useTodoAPI.tsx
  useEffect(() => {
    axios
      .get("http://localhost:3000")
      .then((response: AxiosResponse<GetTodoTypes>) => {
        console.log(response.data.todos);
        const { todos } = response.data;
        setTodos(todos);
      });
  }, []);

これで型宣言ができたのではないでしょうか。

これでリファクタリングの作業は以上です。
フロントエンド側もだいぶすっきりしたのではないでしょうか。
App.tsxの部分には表示関わる部分で、useTodoAPI.tsxの部分には処理を書いていることで、表示と処理を切り分けることができたかと思います。

最後にリファクタリングまで終えたコードのgithubのURLを載せておきますので、どうしてもうまくいかないなどあれば確認してみてください。
READMEにgit clone後の手順を載せていますので、そちらもご確認ください。
https://github.com/har2525life/zenn_test

Discussion