useQueryをクリックイベントで使用する方法

📕 新コースを公開しました。→クーポン掲載ページ

Nextjs(やReactjs)で開発している中で、React Query の useQuery を使うと結果をキャッシュ出来て便利です。

ただuseQueryをonClick時などに使いたい場合、コンポーネントの中の関数内で実行することになります。hooksのルールに反しているのでエラーが発生してしまいます。

Hooksのルール

Hooksにはルール(Breaking the Rules of Hooks)があります。

紹介されているGoodケース

function Counter() {
  // ✅ Good: top-level in a function component
  const [count, setCount] = useState(0);
  // ...
}

function useWindowWidth() {
  // ✅ Good: top-level in a custom Hook
  const [width, setWidth] = useState(window.innerWidth);
  // ...
}

紹介されているBadケース

function Bad1() {
  function handleClick() {
    // 🔴 Bad: inside an event handler (to fix, move it outside!)
    const theme = useContext(ThemeContext);
  }
  // ...
}

function Bad2() {
  const style = useMemo(() => {
    // 🔴 Bad: inside useMemo (to fix, move it outside!)
    const theme = useContext(ThemeContext);
    return createStyle(theme);
  });
  // ...
}

class Bad3 extends React.Component {
  render() {
    // 🔴 Bad: inside a class component
    useEffect(() => {})
    // ...
  }
}

ダメなケース

ルールに反するので、関数コンポーネントのトップレベルで使用せずに、イベントハンドラーで実行するとエラーになります。

// イベントハンドラー内で読んでしまっているBadケース
import { FC } from "react";
import { useQuery } from "react-query";

const SampleComponent: FC = () => {
  const handleClick = async () => {
    const { data } = useQuery(
      ["sample-query-key"],
      async () => await fetch("/api/sample-fetch").then((res) => res.json()),
      {
        enabled: false,
      }
    );
  };

  return (
    <div>
      <h1>Favorite MANGA</h1>
      <button onClick={handleClick}>Show Manga</button>
      {/* {data && (
        <>
          <h1>{data.title}</h1>
          <div>
            <img src={data && data.imgSrc} width="250px" height="250px" />
          </div>
        </>
      )} */}
    </div>
  );
};

export default SampleComponent;

https://reactjs.org/warnings/invalid-hook-call-warning.html

とはいえ、トップレベルに記載すると、初回レンダリング時に実行されるので、クリックイベントと連動させることができません。

// 通常の書き方ではクリックイベントと連動しない
import { FC } from "react";
import { useQuery } from "react-query";

const SampleComponent: FC = () => {
  const { data } = useQuery(
    ["sample-query-key"],
    async () => await fetch("/api/sample-fetch").then((res) => res.json())
  );

  const handleClick = async () => {};

  return (
    <div>
      <h1>Favorite MANGA</h1>
      <button onClick={handleClick}>Show Manga</button>
      {data && (
        <>
          <h1>{data.title}</h1>
          <div>
            <img src={data && data.imgSrc} width="250px" height="250px" />
          </div>
        </>
      )}
    </div>
  );
};

export default SampleComponent;

イベントと連動させて再フェッチする方法

useQueryにはrefetchがあり、任意のタイミングでフェッチさせることができます。refetch関数をイベントハンドラー内で実行すればOKです。

クリックイベントに連動させる場合は下記のような感じです。

import { FC } from "react";
import { useQuery } from "react-query";

const SampleComponent: FC = () => {
  const { data, refetch } = useQuery(
    ["sample-query-key"],
    async () => await fetch("/api/sample-fetch").then((res) => res.json()),
    {
      enabled: false,
    }
  );

  const handleClick = async () => {
    refetch();
  };

  return (
    <div>
      <h1>Favorite MANGA</h1>
      <button onClick={handleClick}>Show Manga</button>
      {data && (
        <>
          <h1>{data.title}</h1>
          <div>
            <img src={data && data.imgSrc} width="250px" height="250px" />
          </div>
        </>
      )}
    </div>
  );
};

export default SampleComponent;

まずuseQueryの返り値からrefetchを取り出し、handleClick関数で実行しています。

ポイントは useQuery の第3引数で設定している enabled: false になります。

これはつけなくてもいいのですが、falseにしない場合は、初回レンダリング時に自動的にfetchされてしまいます。あくまでボタンなどのイベントが発生したときにのみfetchしたい場合は false にしておきます。

usequery-refetch (2).gif

参考

🎓✍️コース一覧

プログラミング関係のビデオコースを提供しています。クーポンも発行していますので、ぜひ一度チェックしてみてください。

Twitter @takumafujimoto

記事を読んでいただきありがとうございます。ツイッターではプログラミング以外についてや、たまにクーポン情報もツイートしたり。。。ツイッターでもお待ちしてます。