📞

REST APIと良い感じに通信するHookを自作する

2022/05/29に公開

ReactでREST APIと通信する際はReact QuerySWRといったライブラリを使用されることが多いと思います。

ただ、そのようなライブラリを使用せずに気軽にAPIを叩きたいということもあると思います。そのような時に自分が作っているカスタムHookをご紹介したいと思います。

コンポーネント内で直接APIと通信する(Bad)

Hookにするメリットを感じられるように、初めにHook化をせずにAPIと通信した例を紹介します。

import { useEffect, useState } from "react";

type Example = {
  title: string;
};

export const BadExample = () => {
  const [data, setData] = useState<Example>();
  const [isLoading, setLoading] = useState(true);
  const [isError, setError] = useState(false);
  
  useEffect(() => {
    // 即時関数
    (async() => {
      try {
        const response = await fetch("/example");
        const data = await response.json();
        setData(data);
      } catch(err) {
        setError(true);
      } finally {
        setLoading(false);
      }
    })()
  }, []);
  
  if(isLoading) {
    return <p>...loading</p>
  }
  
  if(isError) {
    return <p>Error!</p>
  }
  
  return <h1>{data?.title}</h1>
};

この例ではコンポーネント内で直接APIと通信しており、コンポーネントとロジックが結びついてしまっています。
このデメリットとして、複数のAPIと通信するコンポーネントではロジックが複雑化することや、API通信を再利用できない(この例で言うとBadExample以外のコンポーネントで/exampleからデータを取得する際に、同じコードをコピペして使い回す必要がある)ことなどが挙げられます。

API通信をHookに分離する(No Bad)

次にAPI通信をしているロジックをそのままHookに切り出した例です。

import { useEffect, useState } from "react";

type Example = {
  title: string;
};

export const useExample = () => {
  const [data, setData] = useState<Example>();
  const [isLoading, setLoading] = useState(true);
  const [isError, setError] = useState(false);
  
  useEffect(() => {
    // 即時関数
    (async() => {
      try {
        const response = await fetch("/example");
        const data = await response.json();
        setData(data);
      } catch(err) {
        setError(true);
      } finally {
        setLoading(false);
      }
    })()
  }, []);
  
  return { data, isLoading, isError };
};
import { useExample } from "./useExample";

export const NoBadExample = () => {
  const { data, isLoading, isError } = useExample();
  
  if(isLoading) {
    return <p>...loading</p>
  }
  
  if(isError) {
    return <p>Error!</p>
  }
  
  return <h1>{data?.title}</h1>
};

この例では、useExampleにAPI通信のロジックや状態を持たせることができており、コンポーネント内をスッキリさせることができました。また、useExampleを使用することで/exampleとのAPI通信を再利用することができます。

しかし、このパターンでは、別のAPIエンドポイントの/examplesと通信をすることになった時に、同じようなHookが量産されてしまいます。

// /examples と通信する
// useExample との違いは data の型と fetch する URL のみ
import { useEffect, useState } from "react";

type Example = {
  title: string;
};

export const useExamples = () => {
  const [data, setData] = useState<Example[]>();  // <-- 型が Example[]
  const [isLoading, setLoading] = useState(true);
  const [isError, setError] = useState(false);
  
  useEffect(() => {
    // 即時関数
    (async() => {
      try {
        const response = await fetch("/examples"); // <-- パスが /examples
        const data = await response.json();
        setData(data);
      } catch(err) {
        setError(true);
      } finally {
        setLoading(false);
      }
    })()
  }, []);
  
  return { data, isLoading, isError };
};

このことから、さらに再利用性の高いHookを作ることがGoodであると自分は思いました。

API通信をする汎用性の高いHookを使用する(Good)

useFetchGenericsdataの型を受け取り、引数でurlを受け取ります。
このHookを用いることで、APIと通信をする度にHookを作成する手間がなくなり、気軽にAPIを叩けるようになります。

import { useEffect, useState } from "react";

export const useFetch = <T>(url: string) => {
  const [data, setData] = useState<T>();  // <-- Generics で受け取った型を data の型とする
  const [isLoading, setLoading] = useState(true);
  const [isError, setError] = useState(false);

  useEffect(() => {
    (async () => {
      try {
        const res = await fetch(url);  // <-- 引数で受け取った url を fetch する
        const data = await res.json();
        setData(data);
      } catch (err) {
        console.error(err);
        setError(true);
      } finally {
        setLoading(false);
      }
    })();
  }, []);

  return { data, isLoading, isError };
};

/exampleからデータを取得する

import { useFetch } from "./useFetch";

type Example = {
  title: string;
}

export const GoodExample = () => {
  const { data, isLoading, isError } = useFetch<Example>("/example");
  
  if(isLoading) {
    return <p>...loading</p>
  }
  
  if(isError) {
    return <p>Error!</p>
  }
  
  return <h1>{data?.title}</h1>
};

/examplesからデータを取得する

import { useFetch } from "./useFetch";

type Example = {
  title: string;
}

export const GoodExamples = () => {
  const { data, isLoading, isError } = useFetch<Example[]>("/examples");
  
    if(isLoading) {
    return <p>...loading</p>
  }
  
  if(isError) {
    return <p>Error!</p>
  }
  
  return (
    <ul>
      {data?.map((d) => (
        <li>{d.title} </li>
      ))}
    </ul>
  );
};

最後に

よくあるロジックの切り出し方として、2番目に紹介したNo Badな方法がよく取られがちだと思いますが、そこから一歩踏み込んで、さらに再利用性の高いHookを作成することがより良い解決法だと思います。昨今ではロジックとコンポーネントを分離することが当たり前になってきているので、その一歩先を考えると良いかもしれませんね。

Discussion