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