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 にしておきます。