カスタムフックを作る具体的な手順(私流)

カスタムフックを作る具体的な手順(私流)

投稿日: 2024年12月02日

Tips
学習振り返り
要約
  • カスタムフックを使うことで、複数のコンポーネントで簡単に再利用できる状態管理が可能になる。
  • 基本的な手順は、既存のコンポーネントをフックとして切り出し、必要な状態や関数を返すこと。
  • 可読性を考慮しながら、フックの汎用性を高めるためのリファクタリング手法として有効。

はじめに

カスタムフックは汎用性の高いものを作ると使いまわせて大変便利です。

また、機能部分の記述が長くなってくるとまとめて外に出したいなんて思うこともあると思います(ないんかな)。

スッキリするので私は機能出したい派です。

やり過ぎて、「フックにするほどでも・・」ってコメントされたことありますけどw(ぐさっ)

この記事では、「うーん。機能部分だけ切り出したいなぁ」ってなった時にカスタムフックにする手順(私流)を解説してみます。

コード

これが切り出す前のコードです。

import { useParams } from "next/navigation";
import { useState, useEffect } from "react";
import { Categories } from "./Categories";
import dayjs from "dayjs";
import Image from "next/image";
interface Post {
  id: number;
  categories: string[];
  createdAt: Date;
  title: string;
  content: string;
}
export const Post: React.FC = () => {
  const { id } = useParams();
  const [post, setPost] = useState<null | Post>(null);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    const fetcher = async () => {
      setIsLoading(true);
      const resp = await fetch(
        `https://XXXXXXXXXX/dev/posts/${id}`
      );
      const data = await resp.json();
      setPost(data.post);
      setIsLoading(false);
    };
    fetcher();
  }, [id]);

  if (isLoading) return <div>読み込み中...</div>;
  if (!post) return <div>記事がありません</div>;

  return (
    <>
      <div className="mx-auto max-w-800px">
        <div className="flex flex-col p-4">
          <Image
            width={800}
            height={500}
            src="https://placehold.jp/800x400.png"
            alt=""
            className="h-auto max-w-full"
          />
          <div className="p-4">
            <div className="flex justify-between">
              <div className="text-gray-600 text-xs">
                {dayjs(post.createdAt).format("YYYY/MM/DD")}
              </div>
              <Categories categories={post.categories}></Categories>
            </div>
            <div className="text-lg mb-4 mt-2">{post.title}</div>
            <div
              className="text-base leading-relaxed"
              dangerouslySetInnerHTML={{ __html: post.content }}
            ></div>
          </div>
        </div>
      </div>
    </>
  );
};

これをカスタムフックにしていきます。

手順細かく刻んでいきますw

手順1:箱用意

まずはフォルダ、ファイルを用意します。

カスタムフックをまとめるフォルダはhooks(Next.jsなら_hooks)という命名で作りましょう。

ファイル名とフック名は必ずuseで始める必要があります。

今回はPostを取得する内容なので、useGetPost.tsにしてみます。

手順2:まるっとコピペ

作ったファイルに丸ごとコピペします。一旦そのままゴソッと行きます。

手順3:コンポーネント名をフック名に変更

export const Post: React.FC = () => {

ここを

export const useGetPost = () => {

に変更します。

手順4:returnの中身全削除して空のオブジェクトに変更

return (
    <>
      <div className="mx-auto max-w-800px">
        <div className="flex flex-col p-4">
          <Image
            width={800}
            height={500}
            src="https://placehold.jp/800x400.png"
            alt=""
            className="h-auto max-w-full"
          />
          <div className="p-4">
            <div className="flex justify-between">
              <div className="text-gray-600 text-xs">
                {dayjs(post.createdAt).format("YYYY/MM/DD")}
              </div>
              <Categories categories={post.categories}></Categories>
            </div>
            <div className="text-lg mb-4 mt-2">{post.title}</div>
            <div
              className="text-base leading-relaxed"
              dangerouslySetInnerHTML={{ __html: post.content }}
            ></div>
          </div>
        </div>
      </div>
    </>
  );

ここを一旦下記のようにしましょう。

return {}

早期リターンしてるところも消しておきます。

 if (isLoading) return <div>読み込み中...</div>;
 if (!post) return <div>記事がありません</div>;

この辺でimportしているけど未使用になるものあると思うので消します。

import { useParams } from "next/navigation";
import { useState, useEffect } from "react";
import { Categories } from "./Categories";
import dayjs from "dayjs";
import Image from "next/image";

Categories、dayjs、Imageはフックでは使わないので消します。

手順5:呼び出し先で使う変数をreturn内に入れていく

基本的にまずは未使用のところを入れていくイメージです。

カスタムフックを作る手順(私流)|ShiftBブログ

postとisLoadingが使われていないので、returnします。

return {post,isLoading}

こんな感じですね。

中にはフック内でも使っているし呼び出し先でも使うものもあります。

そういったものは後で気づくと思うので、気付いたタイミングで追加していけばOKです。

完成

import { useParams } from "next/navigation";
import { useState, useEffect } from "react";
interface Post {
  id: number;
  categories: string[];
  createdAt: Date;
  title: string;
  content: string;
}
export const useGetPost = () => {
  const { id } = useParams();
  const [post, setPost] = useState<null | Post>(null);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    const fetcher = async () => {
      setIsLoading(true);
      const resp = await fetch(
        `https://XXXXXXXXXX/dev/posts/${id}`
      );
      const data = await resp.json();
      setPost(data.post);
      setIsLoading(false);
    };
    fetcher();
  }, [id]);

  return { post, isLoading };
};

カスタムフックの出来上がり!!

使う側

import

まずは作ったフックをインポートします。

import { useGetPost } from "./_hooks/useGetPost";

修正作業

returnより上の機能部分を消します。

 const { id } = useParams();
  const [post, setPost] = useState<null | Post>(null);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    const fetcher = async () => {
      setIsLoading(true);
      const resp = await fetch(
       `https://XXXXXXXXXX/dev/posts/${id}`
      );
      const data = await resp.json();
      setPost(data.post);
      setIsLoading(false);
    };
    fetcher();
  }, [id]);

ここまるっと消します。

そしてインポートしたフックから必要なものを取得します。

const { post, isLoading } = useGetPost();

そして不要なimport削除します。

ここでは

import { useParams } from "next/navigation";
import { useState, useEffect } from "react";

この二行がもう不要なので削除します。

今回はPost型の定義もコンポーネント側では必要ないので消しちゃいます。

完成

import { useGetPost } from "./_hooks/useGetPost";
import { Categories } from "./Categories";
import dayjs from "dayjs";
import Image from "next/image";

export const Post: React.FC = () => {
  const { post, isLoading } = useGetPost();

  if (isLoading) return <div>読み込み中...</div>;
  if (!post) return <div>記事がありません</div>;

  return (
    <>
      <div className="mx-auto max-w-800px">
        <div className="flex flex-col p-4">
          <Image
            width={800}
            height={500}
            src="https://placehold.jp/800x400.png"
            alt=""
            className="h-auto max-w-full"
          />
          <div className="p-4">
            <div className="flex justify-between">
              <div className="text-gray-600 text-xs">
                {dayjs(post.createdAt).format("YYYY/MM/DD")}
              </div>
              <Categories categories={post.categories}></Categories>
            </div>
            <div className="text-lg mb-4 mt-2">{post.title}</div>
            <div
              className="text-base leading-relaxed"
              dangerouslySetInnerHTML={{ __html: post.content }}
            ></div>
          </div>
        </div>
      </div>
    </>
  );
};

以上です! !

機能部分がそんな長くなければ、切り出さない方が可読性は上がる(と指摘された)ので、なんでもかんでもカスタムフックにしたらいいというわけではないです!再利用できる場合はフックにしたらOKです!

作成するメリット

このカスタムフックを使えば、Postのデータがたった1行で取得できるようになりました!

const { post, isLoading } = useGetPost();

この1行で、記事のデータとその読み込み状態を管理できるようになり、コンポーネント内のコードがすっきりします。

複数のコンポーネントで取得する必要がある場合もこの一行でいいのは助かりますよね!

おわりに

意外とそれだけ?って感じじゃないですか?

汎用性を上げようと思うと型引数受け取ったり色々考えることも増えてきますが、機能部分切り出すだけならこれで大丈夫です。

慣れてくると最初から カスタムフックにしたりすることもありますが、一旦UI作りながら色んな関数書いてロジック考えてる場合は、そこまでできなくてあとからフックにしたりしなかったりです。

リファクタリングの第一歩としてやってみたいと思ったら参考にしていただけると嬉しいです!

シェア!

Threads
user
吉本茜
山口在住/二児の母/育休中
Loading...
記事一覧に戻る
Threads
0