🉐

知っていたらお得!ReactでAPI通信Logicの書き方

2023/03/12に公開

はじめる

Reactを使っているけど、なんとなくコードが臭い・・と思われる方いらっしゃらないでしょうか。今回はReactのアプリケーションを作成する中で、Network通信(API通信)をもっと香ばしい香りがするコードの書き方を紹介さしようと思います。

まず、下記のコードも見てください。

なんとなく・・ちょっとくっさいコード

export default function Videos() {
  const {
    isLoading,
    error,
    data: videos,
  } = useQuery(["videos", keyword], () =>
    axios.get("https:exampleURL").then((res) => res.data)
  );

  return (
   <>
      {isLoading && <p>Loading...</p>}
      {error && <p>Something is wrong 😑</p>}
      {videos && (
        <ul className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-2 gap-y-4">
          {videos.map((video) => (
            <VideoCard key={video.id} video={video} />
          ))}
        </ul>
      )}
    </>
  );
}

上のコードで何かおかしいところ見つかりましたでしょうか?そうですね。UIの記述とNetwork通信 Logicの記述が1つのComponent内に作成されていますね。これってコードが長くなれば長くなるほど、読みにくくなるので、このコードを引き継いた人がいれば「誰が書いたん?」と思っちゃうかもしれませんね。

しかも、誰からこのコードを開いてみると、Backend側のどのAPIにどんなparameterを送るのかが丸見えになっちゃいます。これはあんまりよろしくないはずです。

useQuery(["videos", keyword], () =>
    axios.get("https:exampleURL").then((res) => res.data)
  );

悪もん:https:exampleURLというURL使えば、videosのlistが取れるんだ!
開発者:あれ?なんかよくわからないところからrequestめっちゃ増えてるんだけど・・

なので、Network通信のLogicを別のファイルに切り出す必要があります。キャップセル化とも言えるんですけど、Logicの中身はわからないけどLogicを使う側はそのLogicを使って機能を実装することができるということですね。まずNetwork通信するところをComponentから切り出してみましょう。

ComponentからAPI Logicを切り出す

export default function Videos() {
  const {
    isLoading,
    error,
    data: videos,
  } = useQuery(["videos", keyword], () => {
    return null;
  });

  return (
    <>
      {isLoading && <p>Loading...</p>}
      {error && <p>Something is wrong 😑</p>}
      {videos && (
        <ul className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-2 gap-y-4">
          {videos.map((video) => (
            <VideoCard key={video.id} video={video} />
          ))}
        </ul>
      )}
    </>
  );
}

一見何も変わってないように見えるのですが、このComponentではAPI通信がどのURLを叩いているのかがわからなくなってきました。これで一旦大丈夫です。

apiフィルダ配下にapi通信のためのlogicを用意し、Componentからはそのlogicを呼び出す

import axios from "axios";

export default class API {
  constructor() {}

  async getVieos() {
    return axios.get("https:exampleURL").then((res) => res.data);
  }
}

API通信のできるclassを作成

import API from "../api/API";

export default function Videos() {
  const {
    isLoading,
    error,
    data: videos,
  } = useQuery(["videos", keyword], ()=> {
    const api = new API();
    return api.getVieos();
  });

  return (
    <>
      ...
    </>
  );
}

classをcomponentでinstance化し、呼び出す。componentを作成する人はメソッドの名前でどんなデータを取得するのか「この場合はvideosを持ってくるだろう。。。」とわかりやすいですね。内部のLogicは隠したままどんな動きをするのかがわかるようになりました。

でもこのままだと他のcomponentでAPI通信をしたい時、またclassをinstance化しないといけないとか、classをinstance化しているcomponentがre-renderingされるためにclassが新たにinstance化されるためとてもリソースをもったいなく費やしている気がしますね。これを直してみましょう。

useContextを使い、api通信のための変数を共有する。かつClassのインスタンス化は最初の一回だけ!

export const ApiContext = createContext();

const api = new API();

export function ApiProvider({ children }) {
  return (
    <ApiContext.Provider value={{ api }}>
      {children}
    </ApiContext.Provider>
  );
}

export function useApi() {
  return useContext(ApiContext);
}

このようにuseContextを利用します。そうすると、API Componentはアプリが起動される最初だけinstance化され、そのinstanceを複数のcomponentで使うことができるようになります。

function App() {
  return (
    <>
      <SearchHeader />
      <ApiProvider>
        <QueryClientProvider client={queryClient}>
          <Outlet />
        </QueryClientProvider>
      </ApiProvider>
    </>
  );
}

こうすると「Outlet」配下のComponentなら、どこでも Api instanceを使えるようになりますね。

export default function Videos() {
  const { api } = useApi();
  const {
    isLoading,
    error,
    data: videos,
  } = useQuery(["videos", keyword], () => api.getVideos());

  return <>...</>;
}

最終的にcomponent内部はこういうふうになります。かなりシンプルで、無駄なリソースも使っていないですね。

Discussion