カスタムフックを使ってコンポーネントをキレイに

# 自己紹介
現在都内の企業でWebエンジニアのインターン生としてお世話になっている大学２年生です！
インターンや個人開発で学んだことや苦労したことを記事にしています！
よろしくお願いします🙇🏻‍♂️

# はじめに
React Hooksの機能であるカスタムフックについてアウトプットしていこうと思います！

# カスタムフックとは
>自分独自のフックを作成することで、コンポーネントからロジックを抽出して再利用可能な関数を作ることが可能である。

[React公式サイト](https://ja.reactjs.org/docs/hooks-custom.html)より

# カスタムフックを使うメリット
- 複数のフックを1つの関数にまとめられる
- コンポーネントのコードが簡潔になる
- ロジックを再利用しやすくなる

この３つのメリットを実際にカスタムフックを作成して解説していく。

:::note warn
間違っている箇所があれば、教えてくださると助かります！
:::

# 実際に作成してみる
### カスタムフック化するコード
```react:OriginalFile.jsx
import React, { memo, useCallback, useEffect, useState } from "react";
import Modal from "react-modal";

export const SUDPost = memo(() => {
  const [posts, setPosts] = useState([]);
  const [modalIsOpen, setModalIsOpen] = useState(false);
  const [edit, setEdit] = useState({ title: "", post: "" });

//投稿データを全取得
  useEffect(() => {
    fetch("http://localhost:8080/timeline/post/get")
      .then((res) => res.json())
      .then((json) => {
        setPosts(json);
      });
  }, []);

  //削除
  const onClickDelete = (id) => {
    window.confirm("本当に削除しますか？") &&
      fetch("http://localhost:8080/timeline/post/delete/" + id, {
        method: "DELETE",
      }).then(() => {
        window.location.reload();
      });
  };

  //複製
  const onClickDuplicate = (id) => {
    fetch("http://localhost:8080/timeline/post/get/" + id)
      .then((res) => res.json())
      .then((json) => {
        const data = {
          title: json.title,
          post: json.post,
        };
        fetch("http://localhost:8080/timeline/post/post", {
          method: "POST",
          headers: {
            Accept: "application/json",
            "Content-Type": "application/json",
          },
          body: JSON.stringify(data),
        }).then(() => {
          alert("複製完了しました。");
          window.location.reload();
        });
      });
  };

  //編集
    const onClickEdit = useCallback((id) => {
    //モーダルを開く
    setModalIsOpen(true);
    fetch("http://localhost:8080/timeline/post/get/" + id)
      .then((res) => res.json())
      .then((json) => {
        //取ってきた投稿を保持
        setEdit(json);
      });
  }, []);

  //フォームの中を変更できるようにする
  const handleEdit = useCallback(
    (e) => {
      const { name, value } = e.target;
      setEdit({ ...edit, [name]: value });
    },
    [edit]
  );

  //更新
  const onClickUpdate = useCallback(
    (id) => {
      const data = {
        title: edit.title,
        post: edit.post,
      };
      fetch("http://localhost:8080/timeline/post/put/" + id, {
        method: "PUT",
        headers: {
          Accept: "application/json",
          "Content-Type": "application/json",
        },
        body: JSON.stringify(data),
      }).then(() => {
        alert("編集完了しました。");
        window.location.reload();
      });
    },
    [edit]
  );

  return (
    <>
      <h3>投稿一覧</h3>
      {posts.map((post) => (
        <ul key={post.id}>
          <li>
            <p>{post.title}</p>
            <p>{post.post}</p>
            <input
              type="submit"
              value="削除"
              onClick={() => onClickDelete(post.id)}
            />
            <input
              type="submit"
              value="複製"
              onClick={() => onClickDuplicate(post.id)}
            />
            <input
              type="submit"
              value="編集"
              onClick={() => onClickEdit(post.id)}
            />
            <Modal isOpen={modalIsOpen} ariaHideApp={false}>
              <input
                type="submit"
                value="閉じる"
                onClick={() => setModalIsOpen(false)}
              />
              <div>
                <div>
                  <label>タイトル</label>
                </div>
                <input
                  type="text"
                  id="title"
                  name="title"
                  value={edit.title}
                  onChange={handleEdit}
                />
                <div>
                  <label>投稿</label>
                </div>
                <input
                  type="text"
                  id="post"
                  name="post"
                  value={edit.post}
                  onChange={handleEdit}
                />
              </div>
              <input
                type="submit"
                value="送信"
                onClick={() => onClickUpdate(post.id)}
              />
            </Modal>
          </li>
        </ul>
      ))}
    </>
  );
});
```
### Step1.削除と複製のロジックを切り離す
```react:usePostDelete.js
//カスタムフックはuseで始まり直後が大文字である関数でなければ作成できません
import { useCallback } from "react";

export const usePostDelete = () => {
  const deletePost = useCallback((id) => {
    fetch("http://localhost:8080/timeline/post/delete/" + id, {
      method: "DELETE",
    }).then(() => {
      window.location.reload();
    });
  }, []);
  return { deletePost };
};
```

```react:usePostDuplicate.jsx
import { useCallback } from "react";

export const usePostDuplicate = () => {
  const duplicatePost = useCallback((id) => {
    fetch("http://localhost:8080/timeline/post/get/" + id)
      .then((res) => res.json())
      .then((json) => {
        const data = {
          title: json.title,
          post: json.post,
        };
        fetch("http://localhost:8080/timeline/post/post", {
          method: "POST",
          headers: {
            Accept: "application/json",
            "Content-Type": "application/json",
          },
          body: JSON.stringify(data),
        }).then(() => {
          alert("複製完了しました。");
          window.location.reload();
        });
      });
  }, []);
  return { duplicatePost };
};
```

```react:OriginalFile.jsx
import React, { memo, useCallback, useEffect, useState } from "react";
import Modal from "react-modal";

import { usePostAllGet } from "../../hooks/post/usePostAllGet";
import { usePostDelete } from "../../hooks/post/usePostDelete";

export const SUDPost = memo(() => {
  const [posts, setPosts] = useState([]);
  const [modalIsOpen, setModalIsOpen] = useState(false);
  const [edit, setEdit] = useState({ title: "", post: "" });
  const { deletePost } = usePostDelete();
  const { duplicatePost } = usePostDuplicate();

//投稿データを全取得
  useEffect(() => {
    fetch("http://localhost:8080/timeline/post/get")
      .then((res) => res.json())
      .then((json) => {
        setPosts(json);
      });
  }, []);

  //削除
  const onClickDelete = (id) => deletePost(id);
  //複製
  const onClickDuplicate = (id) => duplicatePost(id);
  //編集
    const onClickEdit = useCallback((id) => {
    //モーダルを開く
    setModalIsOpen(true);
    fetch("http://localhost:8080/timeline/post/get/" + id)
      .then((res) => res.json())
      .then((json) => {
        //取ってきた投稿を保持
        setEdit(json);
      });
  }, []);

  //フォームの中を変更できるようにする
  const handleEdit = useCallback(
    (e) => {
      const { name, value } = e.target;
      setEdit({ ...edit, [name]: value });
    },
    [edit]
  );

  //更新
  const onClickUpdate = useCallback(
    (id) => {
      const data = {
        title: edit.title,
        post: edit.post,
      };
      fetch("http://localhost:8080/timeline/post/put/" + id, {
        method: "PUT",
        headers: {
          Accept: "application/json",
          "Content-Type": "application/json",
        },
        body: JSON.stringify(data),
      }).then(() => {
        alert("編集完了しました。");
        window.location.reload();
      });
    },
    [edit]
  );

  return (
    <>
      <h3>投稿一覧</h3>
      {posts.map((post) => (
        <ul key={post.id}>
          <li>
            <p>{post.title}</p>
            <p>{post.post}</p>
            <input
              type="submit"
              value="削除"
              onClick={() => onClickDelete(post.id)}
            />
            <input
              type="submit"
              value="複製"
              onClick={() => onClickDuplicate(post.id)}
            />
            <input
              type="submit"
              value="編集"
              onClick={() => onClickEdit(post.id)}
            />
            <Modal isOpen={modalIsOpen} ariaHideApp={false}>
              <input
                type="submit"
                value="閉じる"
                onClick={() => setModalIsOpen(false)}
              />
              <div>
                <div>
                  <label>タイトル</label>
                </div>
                <input
                  type="text"
                  id="title"
                  name="title"
                  value={edit.title}
                  onChange={handleEdit}
                />
                <div>
                  <label>投稿</label>
                </div>
                <input
                  type="text"
                  id="post"
                  name="post"
                  value={edit.post}
                  onChange={handleEdit}
                />
              </div>
              <input
                type="submit"
                value="送信"
                onClick={() => onClickUpdate(post.id)}
              />
            </Modal>
          </li>
        </ul>
      ))}
    </>
  );
});
```
### Step2.投稿データを全取得するロジックを切り離す
```react:usePostAllGet.js
import { useEffect, useState } from "react";

export const usePostAllGet = () => {
  const [posts, setPosts] = useState([]);
  useEffect(() => {
    //投稿データを全取得
    fetch("http://localhost:8080/timeline/post/get")
      .then((res) => res.json())
      .then((json) => {
        setPosts(json);
      });
  }, []);
  return { posts };
};
```
```react:OriginalFile.jsx
import React, { memo, useCallback, useState } from "react";
import Modal from "react-modal";

import { usePostAllGet } from "../../hooks/post/usePostAllGet";
import { usePostDelete } from "../../hooks/post/usePostDelete";
import { usePostDuplicate } from "../../hooks/post/usePostDuplicate";

export const SUDPost = memo(() => {
  const [modalIsOpen, setModalIsOpen] = useState(false);
  const [edit, setEdit] = useState({ title: "", post: "" });
  const { posts } = usePostAllGet();
  const { deletePost } = usePostDelete();
  const { duplicatePost } = usePostDuplicate();

  //削除
  const onClickDelete = (id) => deletePost(id);
  //複製
  const onClickDuplicate = (id) => duplicatePost(id);
  //編集
    const onClickEdit = useCallback((id) => {
    //モーダルを開く
    setModalIsOpen(true);
    fetch("http://localhost:8080/timeline/post/get/" + id)
      .then((res) => res.json())
      .then((json) => {
        //取ってきた投稿を保持
        setEdit(json);
      });
  }, []);

  //フォームの中を変更できるようにする
  const handleEdit = useCallback(
    (e) => {
      const { name, value } = e.target;
      setEdit({ ...edit, [name]: value });
    },
    [edit]
  );

  //更新
  const onClickUpdate = useCallback(
    (id) => {
      const data = {
        title: edit.title,
        post: edit.post,
      };
      fetch("http://localhost:8080/timeline/post/put/" + id, {
        method: "PUT",
        headers: {
          Accept: "application/json",
          "Content-Type": "application/json",
        },
        body: JSON.stringify(data),
      }).then(() => {
        alert("編集完了しました。");
        window.location.reload();
      });
    },
    [edit]
  );

  return (
    <>
      <h3>投稿一覧</h3>
      {posts.map((post) => (
        <ul key={post.id}>
          <li>
            <p>{post.title}</p>
            <p>{post.post}</p>
            <input
              type="submit"
              value="削除"
              onClick={() => onClickDelete(post.id)}
            />
            <input
              type="submit"
              value="複製"
              onClick={() => onClickDuplicate(post.id)}
            />
            <input
              type="submit"
              value="編集"
              onClick={() => onClickEdit(post.id)}
            />
            <Modal isOpen={modalIsOpen} ariaHideApp={false}>
              <input
                type="submit"
                value="閉じる"
                onClick={() => setModalIsOpen(false)}
              />
              <div>
                <div>
                  <label>タイトル</label>
                </div>
                <input
                  type="text"
                  id="title"
                  name="title"
                  value={edit.title}
                  onChange={handleEdit}
                />
                <div>
                  <label>投稿</label>
                </div>
                <input
                  type="text"
                  id="post"
                  name="post"
                  value={edit.post}
                  onChange={handleEdit}
                />
              </div>
              <input
                type="submit"
                value="送信"
                onClick={() => onClickUpdate(post.id)}
              />
            </Modal>
          </li>
        </ul>
      ))}
    </>
  );
});
```
### Step3:編集のロジックを切り離す
```react:usePostEdit.js
import { useCallback, useState } from "react";

export const usePostEdit = () => {
  const [edit, setEdit] = useState({ title: "", post: "" });

  //指定した投稿データを取得
  const getPost = useCallback((id) => {
    fetch("http://localhost:8080/timeline/post/get/" + id)
      .then((res) => res.json())
      .then((json) => {
        setEdit(json);
      });
  }, []);

  //フォームの中を変更できるようにする
  const handleEdit = useCallback(
    (e) => {
      const { name, value } = e.target;
      setEdit({ ...edit, [name]: value });
    },
    [edit]
  );

  //投稿を更新
  const updatePost = useCallback(
    (id) => {
      const data = {
        title: edit.title,
        post: edit.post,
      };
      fetch("http://localhost:8080/timeline/post/put/" + id, {
        method: "PUT",
        headers: {
          Accept: "application/json",
          "Content-Type": "application/json",
        },
        body: JSON.stringify(data),
      }).then(() => {
        alert("編集完了しました。");
        window.location = "/";
      });
    },
    [edit]
  );

  return { edit, getPost, handleEdit, updatePost };
};
```
```react:OriginalFile.jsx
import React, { memo, useState } from "react";
import Modal from "react-modal";

import { usePostAllGet } from "../../hooks/post/usePostAllGet";
import { usePostDelete } from "../../hooks/post/usePostDelete";
import { usePostDuplicate } from "../../hooks/post/usePostDuplicate";
import { usePostEdit } from "../../hooks/post/usePostEdit";

export const SUDPost = memo(() => {
  const [modalIsOpen, setModalIsOpen] = useState(false);
  const { posts } = usePostAllGet();
  const { deletePost } = usePostDelete();
  const { duplicatePost } = usePostDuplicate();
  const { edit, getPost, handleEdit, updatePost } = usePostEdit();

  //削除
  const onClickDelete = (id) => deletePost(id);
  //複製
  const onClickDuplicate = (id) => duplicatePost(id);
  //編集
  const onClickModalIsOpen = (id) => {
    setModalIsOpen(true);
    getPost(id);
  };
  const handleEditChange = (id) => handleEdit(id);
  const onClickUpdate = (id) => updatePost(id);

  return (
    <>
      <h3>投稿一覧</h3>
      {posts.map((post) => (
        <ul key={post.id}>
          <li>
            <p>{post.title}</p>
            <p>{post.post}</p>
            <input
              type="submit"
              value="削除"
              onClick={() => onClickDelete(post.id)}
            />
            <input
              type="submit"
              value="複製"
              onClick={() => onClickDuplicate(post.id)}
            />
            <input
              type="submit"
              value="編集"
              onClick={() => onClickModalIsOpen(post.id)}
            />
            <Modal isOpen={modalIsOpen} ariaHideApp={false}>
              <input
                type="submit"
                value="閉じる"
                onClick={() => setModalIsOpen(false)}
              />
              <div>
                <div>
                  <label>タイトル</label>
                </div>
                <input
                  type="text"
                  id="title"
                  name="title"
                  value={edit.title}
                  onChange={handleEditChange}
                />
                <div>
                  <label>投稿</label>
                </div>
                <input
                  type="text"
                  id="post"
                  name="post"
                  value={edit.post}
                  onChange={handleEditChange}
                />
              </div>
              <input
                type="submit"
                value="送信"
                onClick={() => onClickUpdate(edit.id)}
              />
            </Modal>
          </li>
        </ul>
      ))}
    </>
  );
});
```
### 最終的にこうなった
```react:usePostDelete.js
import { useCallback } from "react";

export const usePostDelete = () => {
  const deletePost = useCallback((id) => {
    fetch("http://localhost:8080/timeline/post/delete/" + id, {
      method: "DELETE",
    }).then(() => {
      window.location.reload();
    });
  }, []);
  return { deletePost };
};
```

```react:usePostDuplicate.jsx
import { useCallback } from "react";

export const usePostDuplicate = () => {
  const duplicatePost = useCallback((id) => {
    fetch("http://localhost:8080/timeline/post/get/" + id)
      .then((res) => res.json())
      .then((json) => {
        const data = {
          title: json.title,
          post: json.post,
        };
        fetch("http://localhost:8080/timeline/post/post", {
          method: "POST",
          headers: {
            Accept: "application/json",
            "Content-Type": "application/json",
          },
          body: JSON.stringify(data),
        }).then(() => {
          alert("複製完了しました。");
          window.location.reload();
        });
      });
  }, []);
  return { duplicatePost };
};
```
```react:usePostAllGet.js
import { useEffect, useState } from "react";

export const usePostAllGet = () => {
  const [posts, setPosts] = useState([]);
  useEffect(() => {
    //投稿データを全取得
    fetch("http://localhost:8080/timeline/post/get")
      .then((res) => res.json())
      .then((json) => {
        setPosts(json);
      });
  }, []);
  return { posts };
};
```
```react:usePostEdit.js
import { useCallback, useState } from "react";

export const usePostEdit = () => {
  const [edit, setEdit] = useState({ title: "", post: "" });

  //指定した投稿データを取得
  const getPost = useCallback((id) => {
    fetch("http://localhost:8080/timeline/post/get/" + id)
      .then((res) => res.json())
      .then((json) => {
        setEdit(json);
      });
  }, []);

  //フォームの中を変更できるようにする
  const handleEdit = useCallback(
    (e) => {
      const { name, value } = e.target;
      setEdit({ ...edit, [name]: value });
    },
    [edit]
  );

  //投稿を更新
  const updatePost = useCallback(
    (id) => {
      const data = {
        title: edit.title,
        post: edit.post,
      };
      fetch("http://localhost:8080/timeline/post/put/" + id, {
        method: "PUT",
        headers: {
          Accept: "application/json",
          "Content-Type": "application/json",
        },
        body: JSON.stringify(data),
      }).then(() => {
        alert("編集完了しました。");
        window.location = "/";
      });
    },
    [edit]
  );

  return { edit, getPost, handleEdit, updatePost };
};
```
```react:OriginalFile.jsx
import React, { memo, useState } from "react";
import Modal from "react-modal";

import { usePostAllGet } from "../../hooks/post/usePostAllGet";
import { usePostDelete } from "../../hooks/post/usePostDelete";
import { usePostDuplicate } from "../../hooks/post/usePostDuplicate";
import { usePostEdit } from "../../hooks/post/usePostEdit";

export const SUDPost = memo(() => {
  const [modalIsOpen, setModalIsOpen] = useState(false);
  const { posts } = usePostAllGet();
  const { deletePost } = usePostDelete();
  const { duplicatePost } = usePostDuplicate();
  const { edit, getPost, handleEdit, updatePost } = usePostEdit();

  //削除
  const onClickDelete = (id) => deletePost(id);
  //複製
  const onClickDuplicate = (id) => duplicatePost(id);
  //編集
  const onClickModalIsOpen = (id) => {
    setModalIsOpen(true);
    getPost(id);
  };
  const handleEditChange = (id) => handleEdit(id);
  const onClickUpdate = (id) => updatePost(id);

  return (
    <>
      <h3>投稿一覧</h3>
      {posts.map((post) => (
        <ul key={post.id}>
          <li>
            <p>{post.title}</p>
            <p>{post.post}</p>
            <input
              type="submit"
              value="削除"
              onClick={() => onClickDelete(post.id)}
            />
            <input
              type="submit"
              value="複製"
              onClick={() => onClickDuplicate(post.id)}
            />
            <input
              type="submit"
              value="編集"
              onClick={() => onClickModalIsOpen(post.id)}
            />
            <Modal isOpen={modalIsOpen} ariaHideApp={false}>
              <input
                type="submit"
                value="閉じる"
                onClick={() => setModalIsOpen(false)}
              />
              <div>
                <div>
                  <label>タイトル</label>
                </div>
                <input
                  type="text"
                  id="title"
                  name="title"
                  value={edit.title}
                  onChange={handleEditChange}
                />
                <div>
                  <label>投稿</label>
                </div>
                <input
                  type="text"
                  id="post"
                  name="post"
                  value={edit.post}
                  onChange={handleEditChange}
                />
              </div>
              <input
                type="submit"
                value="送信"
                onClick={() => onClickUpdate(edit.id)}
              />
            </Modal>
          </li>
        </ul>
      ))}
    </>
  );
});
```
最終的にOriginalFile.jsxはコードが簡潔になり見やすくなったと思います！

# 参考

https://zenn.dev/izuchy/articles/5cec4c2318caff6490de#%E3%82%82%E3%81%A3%E3%81%A8%E3%82%AB%E3%82%B9%E3%82%BF%E3%83%A0%E3%83%95%E3%83%83%E3%82%AF%E3%81%AB%E8%A7%A6%E3%82%8C%E3%81%A6%E3%81%BF%E3%82%88%E3%81%86

https://nishinatoshiharu.com/overview-custom-hook/

https://blog.uhy.ooo/entry/2021-02-23/usecallback-custom-hooks/

# 最後に
今回はhooksの機能であるカスタムフックをアウトプットしました！
今回だけではないですが、間違った理解をしてしまっていることがありますので、ここ間違ってるよ~などございましたらぜひ教えて下さい！
ではいい一日を😁