Reactで開発をしていた際、
useEffectで宣言された処理をボタンの押下(onClick)で呼び出そうとしたのですが詰まってしまったので備忘録として記事にしたいと思います。
やりたいこと
ボタンを押下した段階でuseEffectで宣言しているsetTimeOut関数を呼び出す。
const [limit, setLimit] = useState<number>(10000);
const [limitPhrase, setLimitPhrase] = useState<string>("");
// ボタンを押下した時の処理を定数timeStartに指定したい
// const timeStart = () =>{}
// この部分(タイマー処理)をボタン押下で発火させたい
useEffect(() => {
const timer = setInterval(() => {
if (limit > 0 && startFlag) {
setLimit((limit) => limit - 10);
setLimitPhrase("");
} else if (limit <= 0) {
setLimitPhrase("finished");
} else {
setLimitPhrase("not start");
}
}, 10);
return () => {
clearInterval(timer);
};
}, [limit]);
return (
<>
<p>{(limit / 1000).toFixed(2)}ms</p>
<p>{limitPhrase}</p>
<button onClick={timeStart}>
START
</button>
</>
);
よくない例
クリックイベントで発火させようとしてonClickの処理内(ここではtimeStart)にuseEffectの記述を入れてしまうとエラーになる(エラー文は後述。)
const timeStart = ()=>{
useEffect(() => {
const timer = setInterval(() => {
if (limit > 0 && startFlag) {
setLimit((limit) => limit - 10);
setLimitPhrase("");
} else if (limit <= 0) {
setLimitPhrase("finished");
} else {
setLimitPhrase("not start");
}
}, 10);
return () => {
clearInterval(timer);
};
}, [limit]);
};
発生するエラー文は次の通り。
React Hook "useEffect" is called in function "timeStart" that is neither a React function component nor a custom React Hook function.
React component names must start with an uppercase letter.
React Hook names must start with the word "use".
翻訳ツールで日本語訳すると
Reactフック「useEffect」は、React関数コンポーネントでもカスタムReactフック関数でもない関数「timeStart」内で呼び出されています。
Reactコンポーネント名は大文字で始める必要があります。Reactフック名は「use」で始める必要があります。
となる。
useEffectのような一部のReact Hooksはトップレベルで呼び出さないとエラーになるため。
(公式ドキュメント)
対策例
- onClickイベントではuseEffect内で使うフラグを設定するようにする。
- useEffect内部で渡されたフラグを使って条件分岐させる。
const [limit, setLimit] = useState<number>(10000);
const [limitPhrase, setLimitPhrase] = useState<string>("");
// ボタンのクリックイベントでは下記のフラグの真偽をスイッチさせる
const [startFlag, setStartFlag] = useState<boolean>(false);
const timeStart = () => {
setStartFlag(true);
};
useEffect(() => {
const timer = setInterval(() => {
// ボタン押下で指定したフラグがtrueの時に処理が走るように変更
if (limit > 0 && startFlag) {
setLimit((limit) => limit - 10);
setLimitPhrase("");
} else if (limit <= 0 && startFlag) {
setLimitPhrase("finished");
} else {
setLimitPhrase("not start");
}
}, 10);
return () => {
clearInterval(timer);
};
}, [limit, startFlag]);
return (
<>
<p>{(limit / 1000).toFixed(2)}ms</p>
<p>{limitPhrase}</p>
<button onClick={timeStart}>
START
</button>
</>
);
上記のようにロジックの視点を変えることで、ボタンの押下に伴ってuseEffectを含む処理を発火させることが可能になる。
最後に
useEffectを単体で使う(例:APIの呼び出し)際と異なり、ボタンの押下などのイベントが絡むケースでは実装に一工夫が必要でした。
同じように詰まっている方々の参考になれば幸いです。
参考資料
(タイマー部分のロジックの参考)