カスタムフックを作る具体的な手順(私流)
投稿日: 2024年12月02日
カスタムフックは汎用性の高いものを作ると使いまわせて大変便利です。
また、機能部分の記述が長くなってくるとまとめて外に出したいなんて思うこともあると思います(ないんかな)。
スッキリするので私は機能出したい派です。
やり過ぎて、「フックにするほどでも・・」ってコメントされたことありますけど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
まずはフォルダ、ファイルを用意します。
カスタムフックをまとめるフォルダはhooks(Next.jsなら_hooks)という命名で作りましょう。
ファイル名とフック名は必ずuseで始める必要があります。
今回はPostを取得する内容なので、useGetPost.tsにしてみます。
作ったファイルに丸ごとコピペします。一旦そのままゴソッと行きます。
export const Post: React.FC = () => {
ここを
export const useGetPost = () => {
に変更します。
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はフックでは使わないので消します。
基本的にまずは未使用のところを入れていくイメージです。
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 { 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作りながら色んな関数書いてロジック考えてる場合は、そこまでできなくてあとからフックにしたりしなかったりです。
リファクタリングの第一歩としてやってみたいと思ったら参考にしていただけると嬉しいです!