TECHSCORE BLOG

クラウドCRMを提供するシナジーマーケティングのエンジニアブログです。

React18であらためてuseEffectを考えてみる

ついにReact18正式版が出ましたね!たくさんの新機能にワクワクしていますo(ツ)9

注目の機能であるuseTransitionやSuspenseについては既に多くのサイトで取り上げられていますので、今回はちょっと地味なuseEffectまわりの変更点を紹介したいと思います。

何がかわった?

useEffect自体が変わったわけではありませんが、StrictModeで開発モード時の動作が以下のように変わりました。

  1. コンポーネントマウント時にアンマウント、再マウントされる
  2. アンマウント後のステータス更新の警告を廃止

一つずつ見ていきましょう。

1.コンポーネントマウント時にアンマウント、再マウントされる

ちょっと何を言っているかわからないかもしれませんが、コンポーネントがマウントされた時、いきなりアンマウントされて、再度マウントされます。(開発モード時のみです!)
この挙動の意図についてはドキュメントに記載されています。

ja.reactjs.org

この挙動によって、以下のようなv17までは1度しか実行されなかったuseEffectが、2回実行されます。

  React.useEffect(() => {
    console.log("マウントされました!")
  }, []);

1度しか実行したくない場合はどうすればいいか?メンテナの方がこちらで修正案を紹介されています。

github.com

<修正例>

  const didEffect = React.useRef(false);
  React.useEffect(() => {
    if (!didEffect.current) {
      didEffect.current = true;
      console.log("マウントされました!");
    }
  }, []);

2.コンポーネントアンマウント後のステータス更新の警告を廃止

これは「あーそうだよね」という人も多いかもしれません。useEffect に限った話ではありませんが、コンポーネントが破棄されたあとにステータス更新処理を行うと、v17では警告が出ていました。

変更の経緯はこちらです。

github.com

例えば以下のような非同期でデータを取得してステートにセットするuseEffectの場合、ステートセット時にユーザが画面遷移してコンポーネントが破棄されているとv17では警告が出ていました。

    const [content, setContent] = React.useState("");

    React.useEffect(() => {
        fetch(url).then(response => (
            response.text()
        )).then(text => {
            //ここで既にアンマウントされていたら警告される
            setContent(text);
        });
    }, [url]);

v18ではこの警告はでません。上記のページにもありますが、例えば以下のような「警告を出さないため」だけの修正が世間で広まってしまったからです。

    const [content, setContent] = React.useState("");

    React.useEffect(() => {
        let isMounted = true;
        fetch(url).then(response => (
            response.text()
        )).then(text => {
            //既にアンマウントされていたらセットしない
            if (isMounted) {
              setContent(text);
            }
        });
        return () => isMounted = false;
    }, [url]);

この修正に意味はありません。setContent()を実行してしまったとしても問題ありません。
警告が本当に伝えたかったことは「アンマウント後にステータス更新されるってことは非同期で何か処理が走りっぱなしだよね?クリーンアップコード実装忘れてない?大丈夫??」です。

たとえば次のuseEffectは危険です。

    React.useEffect(() => {
      setInterval(() => console.log("イェーイ!!"), 1000);
    }, []);

画面遷移してコンポーネントが破棄されても永遠にイェーイ!!されます。さらに再度コンポーネントがマウントされると重複して、、、、バ〇バインです。。。
こういったことになっていないか?というのが先ほどの警告の内容です。

    React.useEffect(() => {
      const id = setInterval(() => console.log("イェーイ!!"), 1000);
      //アンマウント時にインターバル解除
      return () => clearInterval(id);
    }, []);

これを踏まえて最初のfetchするuseEffectを修正します。

    React.useEffect(() => {
        const controller = new AbortController();
        fetch(url, { signal: controller.signal }).then(response => (
            response.text()
        )).then(text => {
            setContent(text)
        });
        return () => controller.abort();
    }, [url]);

「アンマウントされていたらステータスを更新しない」ではなく、「アンマウントされたら処理を中断する」です。(エラーハンドリングは省略しています)

今後のアップデートに向けて

非常に利用シーンの多い useEffectですが、今後のReact並行処理機能のアップデートに備えて「厳格」に実装していくことが求められています。
ドキュメントや開発モードの挙動変更から、開発陣の強い意志が感じられます!

いますぐに何か問題が起きることはありませんが、今後のアップデートで突然動作しなくなることもありうるので、ライフサイクル/依存データ/冪等性を考えてひとつひとつ丁寧に実装していきましょう。