useMemoに非同期処理を書いてはいけない理由
恥ずかしながら最近知ったので、備忘録としてまとめておきます...
発端
例えば以下のようなコードを書いたとします。
const fetchData = () => {
return new Promise((resolve) => {
setTimeout(() => {
console.log("非同期処理が完了しました");
resolve("非同期データ");
}, 1000);
});
};
const result = useMemo(() => {
fetchData().then(() => {
return false;
});
return true;
}, []);
console.log(result); // true
このコードを見て「fetchData
が終わったら false
を返したい」と思っても、実際には result
には常に true
が入ります。
なぜこうなるのか?
React の useMemo
は、純粋に同期的な計算の結果をメモ化するためのフックです。
const value = useMemo(() => {
// 重い計算
return someHeavyComputation();
}, [dep]);
上のように「ある依存関係に基づく値をキャッシュする」目的で使われます。
一方、今回のように useMemo
の中で非同期処理を行っても、React はその非同期処理の完了を待ってくれません。つまり
useMemo(() => {
fetchData().then(() => {
return false;
});
return true;
}, []);
この場合 fetchData().then(...);
は非同期で実行されますが、useMemo
自体はすぐに true
を返して終了します。
さらに、.then(() => return false)
としても、その false
は Promise チェーンの中だけの話で、useMemo
の返り値にはなりません。したがって result
は常に true
のままとなります。
useMemo
に非同期処理を書く
やってはいけない:そもそも useMemo(async () => { ... })
のような書き方自体、React の思想に反します。useMemo
は副作用を含まない、同期的な関数であるべきだからです。
✅ 非同期処理や副作用は
useEffect
に書くべきです
useEffect
+ useState
を使う
正しい書き方:非同期処理の結果を使いたい場合は、以下のように useState
と useEffect
を組み合わせるのが正解です。
const [result, setResult] = useState(true);
useEffect(() => {
fetchData().then(() => {
setResult(false);
});
}, []);
これで、非同期処理が完了した後に result
の値が更新され、コンポーネントも再レンダリングされます。
おわりに
useMemo
を「初期化に使えるかも」と思って非同期処理を入れてしまうと、思わぬ落とし穴にはまります。特に大規模なコードベースや他人が書いたコードだと、処理の流れを見誤ってバグを生みかねません。
もし他にも良いパターンや注意点があれば教えてもらえると嬉しいです!
Discussion