Reactのカスタムフックについて学習した。
Table of contents
Open Table of contents
カスタムフックとは
これまで学んださまざまな基本的なReactのフックを組み合わせて、再利用可能なロジックを作成する仕組み。
カスタムフックを使用することで、ロジックの再利用が可能になり、テストも行いやすくなる。
カスタムフックの作成方法
Reactのフックを利用した関数を作成し、useで始まる関数名をつければカスタムフックとして扱うことができる。
function useCustomHook() {
処理を記述
}
フォルダ構成
カスタムフックを作成する際、下記のようなフォルダ構成に特に決まりはないが、ある程度の決まりは必要。 下記サイトがわかりやすくまとめられていた。 [https://levtech.jp/media/article/column/detail_711/]
個人的には下記のような構成が管理しやすい。(上記URLの「技術別分類」に近い形)
src/
├─ hooks/
│ ├─ useCounter.ts
│ ├─ useFetch.ts
│ └─ useWindowSize.ts
├─ components/
└─ pages/
カスタムフックの実例
カウントアップ
カウントアップの処理に関するフックuseCounterを作成。
countとhandleClickを返す。
import { useState } from "react";
export function useCounter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount((prev) => prev + 1)
}
return {
count,
handleClick,
}
}
利用する側は下記のようになる。
useCounterから返されるcountは数値の表示、handleClickはbuttonのクリックイベントに割り当てることで、カウントアップ機能を分割したコードとなっている。
import { useCounter } from "./useCounter";
function App() {
const { count, handleClick } = useCounter();
return (
<div>
<p>{count}</p>
<button onClick={handleClick}>カウントアップ</button>
</div>
);
}
API取得
APIを取得するためのカスタムフックuseFetchを作成。
useFetchはurlを受け取り、postsとloadingを返す。
useFetchの実装は下記のようになる。
意図的にloadingをfalseにするタイミングを1秒遅らせている。
import { useState, useEffect } from "react";
type Post = {
userId: number;
id: number;
title: string;
body: string;
};
export function useFetch(url: string) {
const [posts, setPosts] = useState<Post[]>([])
const [loading, setLoading] = useState<boolean>(true)
useEffect(() => {
async function getPosts() {
try {
const res = await fetch(url);
if (!res.ok) {
throw new Error(`HTTP Error: ${res.status}`);
}
const json: Post[] = await res.json();
console.log(json);
setPosts(json);
// 意図的に1秒後にloadingをfalseにしている
setTimeout(() => {
setLoading(false);
}, 1000)
} catch (error) {
console.error(error);
} finally {
setLoading(false);
}
}
getPosts();
}, [url])
return { posts, loading };
}
利用する側は下記のようになる。
useFetchにURLを渡し、fetchでデータを取得。
postsには取得したデータが配列形式で入っているため、mapを使って各要素を表示している。
loadingはtrueの間は<p>Loading...</p>を表示させ、ローディング中であることを示している。
useState使って管理しているので、loadingの値が変わると再レンダリングが発生し、Loading表示と取得したデータ表示が切り替わる。
import { useFetch } from "./useFetch";
function App() {
const { posts, loading } = useFetch("https://jsonplaceholder.typicode.com/posts");
return (
<div>
{loading ? <p>Loading...</p> : posts.map(post =>{
return (
<div key={post.id}>
<p>POST ID: {post.id}, UserID: {post.userId}</p>
<p>{post.title}</p>
<p>{post.body}</p>
</div>
)
})}
</div>
);
}
画面サイズ取得
window.innerWidthとwindow.innerHeightを使って画面サイズを取得→useStateで値を保持し、useEffectでリサイズイベントを監視して値を更新する。
useEffectでresizeイベントを登録し、画面サイズが変更されたときにstateを更新している。
import { useState, useEffect } from "react";
export function useWindowSize() {
const [width, setWidth] = useState(window.innerWidth);
const [height, setHeight] = useState(window.innerHeight);
useEffect(() => {
const handleResize = () => {
setWidth(window.innerWidth)
setHeight(window.innerHeight)
}
window.addEventListener("resize", handleResize)
return () => {
window.removeEventListener("resize", handleResize)
}
}, [])
return { width, height };
}
利用する側は下記のようになる。
useWindowSizeを読み込み、widthとheightを取得。
widthとheightを使って画面サイズに応じた表示を行う。
下記はMobile、Tablet、Desktopの画面サイズに応じた表示を行う例。
import { useWindowSize } from "./useWindowSize";
function App() {
const { width, height } = useWindowSize();
return (
<div>
<p>Window size: {width} x {height}</p>
{width < 500 && <p>Mobile view</p>}
{(width >= 500 && width < 768) && <p>Tablet view</p>}
{width >= 768 && <p>Desktop view</p>}
</div>
);
}
まとめ
これまで勉強してきた各フックを活用して、共通のロジックをフックとして登録できるカスタムフックは非常に便利。 またテストを行う際も、「特定の機能のみテストする」ということも容易となるので、管理もしやすくなる。 カスタムフックはUIを再利用するものではなく、ロジックを再利用するための仕組みである。