🤫

【React】そのuseEffect、消えるよ(非同期処理、useQueryの話)

2022/12/15に公開

概要

タイトルは某テニス漫画のセリフパロです

useQueryの使い方を理解して感動したので紹介します。
useQueryを使えば無駄なstateやuseEffectを書く必要がなくなりすごくスッキリすると思います。

この記事で書いたソースコード↓
https://codesandbox.io/s/usequery-nr5dl2?file=/src/App.js

useEffectを使う書き方

じゃあまずuseQueryを使わないときはどう書いてたかというと以下のようにuseEffectを使ってその中でfetch処理を書いてsetStateしていました。
こうなると取得したデータのstate、ローディング中のstateなどなどstateが増えがちだし関連が分かりにくくなると思います。
あとuseEffect内の処理でasync使おうとするとちょっと気持ち悪い書き方する必要があったりuseEffect内でsetStateしなきゃいけなかったり結構めんどいと思います。

Effect.js
import React, { useState, useEffect } from "react";

async function getDataAsync() {
  const response = await fetch("https://jsonplaceholder.typicode.com/todos/1");
  if (!response.ok) {
    throw new Error("エラー");
  }
  const json = await response.json();
  return JSON.stringify(json);
}

export const Effect = () => {
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState(false);
  const [data, setData] = useState();

  useEffect(() => {
    (async () => {
      setIsLoading(true);
      try {
        const res = await getDataAsync();
        setData(res);
      } catch (e) {
        setError(true);
        console.log(e);
      } finally {
        setIsLoading(false);
      }
    })();
  }, []);

  if (isLoading) {
    return <>Loading...</>;
  }

  if (error) {
    return <>Error</>;
  }

  return <>{data}</>;
};

useQueryを使う書き方

じゃあuseQueryを使うとどれくらい簡潔に書けるのかを見ていきましょう。
(別にuseQueryじゃなくて他のライブラリでもいいんですけど今回は私の好みでuseQueryを使った書き方を紹介します。)

インポート

npm install react-query

本記事で使ったバージョン↓
"react-query": "3.39.2"

useQueryの設定

次にuseQueryの使い方ですがまず設定が必要です。設定にはQueryClientとQueryClientProviderを使用します。
<QueryClientProvider client={queryClient}></QueryClientProvider>で囲った部分に設定が反映されるのでAppなどの一番上でやると楽です。

App.js
import "./styles.css";
import { QueryClient, QueryClientProvider } from "react-query";
import { Query } from "./Query";

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      retry: false,
      refetchOnWindowFocus: false
    }
  }
});

export default function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <div className="App">
        <h1>Hello CodeSandbox</h1>
        <h2>Start editing to see some magic happen!</h2>
        <Query />
      </div>
    </QueryClientProvider>
  );
}

useQueryを使ったコード

次にuseQueryを使うコード。useEffect使ったコードより簡潔なのが分かると思います。

Query.js
import React from "react";
import { useQuery } from "react-query";

async function getDataAsync() {
  const response = await fetch("https://jsonplaceholder.typicode.com/todos/1");
  const json = await response.json();
  return JSON.stringify(json);
}

export const Query = () => {
  const { isLoading, error, data } = useQuery({
    queryKey: "xxx",
    queryFn: getDataAsync,
    cacheTime: 0
  });

  if (isLoading) {
    return <>Loading...</>;
  }

  if (error) {
    return <>Error</>;
  }

  return <>{data}</>;
};

ではクリティカルなとこだけ解説します。
useQueryの戻り値は色々ありますがよく使うのはisLoading,error,dataの3つと思います。名前の通りisLoadingでデータ取得中かどうか、errorはエラーが起きたかどうか、dataに取得したデータが入ります。useEffectを使ってた時は自分でsetStateしてたのがuseQueryは自動で状態入れてくれるので便利ですね。

export const Query = () => {
  const { isLoading, error, data } = useQuery({
    queryKey: "xxx",
    queryFn: getDataAsync,
    cacheTime: 0
  });

再フェッチするには?

でもこれだと最初の一発目にしかデータ取れなくね?イベントでデータ取り直したい時どうするんだよと思うでしょう。
安心してください。実は最初の引数のqueryKeyは依存配列を渡せます。つまりuseEffectの第2引数と同じ使い方ができます。

例えば親コンポーネントからpropsを渡してuseQueryはその値が変化すると値を取り直すというコードは↓のようになります。

App.js
export default function App() {
  const [id, setId] = useState(1);

  const onClick = () => {
    setId((prevId) => prevId + 1);
  };

  return (
    <QueryClientProvider client={queryClient}>
      <div className="App">
        <h1>Hello CodeSandbox</h1>
        <h2>Start editing to see some magic happen!</h2>
        <button onClick={onClick}>CHANGE</button>
        <p></p>
        <Query id={id} />
        <p></p>
        <Effect id={id} />
      </div>
    </QueryClientProvider>
  );
}
Query.js
// idの値でurlを変更する
async function getDataAsync(id) {
  const url = "https://jsonplaceholder.typicode.com/todos/" + id.toString();
  const response = await fetch(url);
  const json = await response.json();
  return JSON.stringify(json);
}

export const Query = ({ id }) => {
  const { isLoading, error, data } = useQuery({
    queryKey: [id], // 依存配列で指定できる([a, b, c]のように複数指定も可)
    queryFn: () => {
      return getDataAsync(id);
    },
    cacheTime: 0
  });

  if (isLoading) {
    return <>Loading...</>;
  }

  if (error) {
    return <>{Error}</>;
  }

  return <>{data}</>;
};

同じ内容をuseEffectで書くと↓のようになります。

useEffectで書いた場合
import React, { useState, useEffect } from "react";

async function getDataAsync(id) {
  const url = "https://jsonplaceholder.typicode.com/todos/" + id.toString();
  const response = await fetch(url);
  if (!response.ok) {
    throw new Error("エラー");
  }
  const json = await response.json();
  return JSON.stringify(json);
}

export const Effect = ({ id }) => {
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState(false);
  const [data, setData] = useState();

  useEffect(() => {
    (async () => {
      setIsLoading(true);
      try {
        const res = await getDataAsync(id);
        setData(res);
      } catch (e) {
        setError(true);
        console.log(e);
      } finally {
        setIsLoading(false);
      }
    })();
  }, [id]);

  if (isLoading) {
    return <>Loading...</>;
  }

  if (error) {
    return <>Error</>;
  }

  return <>{data}</>;
};

useQueryとuseEffectのコードを比較するとuseQueryの方が簡潔に書けてると思います(個人的感想)。
あとuseQueryは結果をcacheしてくれるのでその辺も良いとこだと思います。(引数のcacheTimeでcacheする時間を指定できる)

戻り値や引数の紹介

次に個人的に使う頻度の高そうな戻り値と引数の紹介をします。

const {
  refetch, // refetchする関数。クエリを手動で再取得したい時に使う。
  status, // ステータスが文字列で分かる。isLoadingとかisErrorとか使うと状態増えるじゃんって人はこっち使うと良いかも。
} = useQuery({
  cacheTime, // データをキャッシュする時間[ms]
  enabled, // false:クエリの自動実行を無効にする。
  initialData, // キャッシュの初期データとして使用される。関数の場合初期化中に1回呼び出される
  onError, // エラーが起きた時実行。引数にエラー
  onSettled, // フェッチ成功時かエラーが起きると実行。引数はデータかエラー
  onSuccess, // フェッチが成功するたびに実行。引数にデータ
  retry, // false:失敗したクエリは再試行されない。true:無限に再試行する。数値の場合その回数まで再試行する。
  staleTime, // キャッシュが古いとみなされる時間[ms]。infinityに設定するとデータが古いとみなされることはない。
})

cacheTimeとstaleTimeについて

cacheTimeはデータをキャッシュする時間。staleTimeはキャッシュが古いと判定する時間です。
例えばcacheTime:1000、staleTime:500の場合、
クエリ実行→300msec経過→ページ再訪問→キャッシュデータを使用
クエリ実行→600msec経過→ページ再訪問→キャッシュデータが画面に表示されるが、バックグラウンドで再フェッチが実行される。

useQueryの戻り値や引数は大量にあるので全部知りたい方は公式参照してください。
https://tanstack.com/query/v4/docs/reference/useQuery?from=reactQueryV3&original=https://react-query-v3.tanstack.com/reference/useQuery

Suspenseモード

useQueryはReact18から使えるようになったSuspenseにも対応しています。
Suspenseモードにするには全体に有効にする方法とコンポーネント毎に有効にする方法があります

全体に設定
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      retry: false,
      refetchOnWindowFocus: false,
      Suspense: true,
    }
  }
});

export default function App() {
  return (
    <QueryClientProvider client={queryClient}>
    この中で使うuseQueryすべてにオプションが反映される
    </QueryClientProvider>
  );
}
個別に設定
export const QuerySuspense = ({ id }) => {
  const { data } = useQuery({
    suspense: true // 引数でオプション指定できる
  });
};

使い方は簡単です。親コンポーネントはSuspenseで子を囲ってデータが取得できるまではfallbackの値が表示されます。

App.js
// 重要じゃないとこは省略
export default function App() {
  const [id, setId] = useState(1);
  const onClick = () => {
    setId((prevId) => prevId + 1);
  };

  return (
    <QueryClientProvider client={queryClient}>
      <div className="App">
        <Suspense fallback={"Loading..."}>
          <QuerySuspense id={id} />
        </Suspense>
      </div>
    </QueryClientProvider>
  );
}

useQueryの方は戻り値がdataだけで良くなりました。

QuerySuspense.js
// 重要じゃないとこは省略
export const QuerySuspense = ({ id }) => {
  const { data } = useQuery({
    queryKey: ["AAA", id],
    queryFn: () => {
      return getDataAsync(id);
    },
    cacheTime: 0,
    suspense: true
  });

  return <>{data}</>;
};

以上です。

Discussion