😫

useMemoに非同期処理を書いてはいけない理由

2025/03/29に公開

恥ずかしながら最近知ったので、備忘録としてまとめておきます...

発端

例えば以下のようなコードを書いたとします。

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 を使う

非同期処理の結果を使いたい場合は、以下のように useStateuseEffect を組み合わせるのが正解です。

const [result, setResult] = useState(true);

useEffect(() => {
  fetchData().then(() => {
    setResult(false);
  });
}, []);

これで、非同期処理が完了した後に result の値が更新され、コンポーネントも再レンダリングされます。

おわりに

useMemo を「初期化に使えるかも」と思って非同期処理を入れてしまうと、思わぬ落とし穴にはまります。特に大規模なコードベースや他人が書いたコードだと、処理の流れを見誤ってバグを生みかねません。

もし他にも良いパターンや注意点があれば教えてもらえると嬉しいです!

Discussion