web-dev-qa-db-ja.com

useEffectの実行順序と、reactフックでの内部クリーンアップロジックとは何ですか?

反応文書によると、useEffectは、useEffect部分を再実行する前にクリーンアップロジックをトリガーします。

エフェクトが関数を返す場合、Reactはクリーンアップの時間になるとそれを実行します...

useEffectはデフォルトで更新を処理するため、更新を処理するための特別なコードはありません。次のエフェクトを適用する前に、前のエフェクトをクリーンアップします...

ただし、requestAnimationFrame内でcancelAnimationFrameおよびuseEffectを使用すると、cancelAnimationFrameがアニメーションを正常に停止しないことがあります。時々、古いアニメーションがまだ存在しているのに気づきましたが、次の効果は別のアニメーションをもたらします。これにより、Webアプリのパフォーマンスの問題が発生します(特に重いDOM要素をレンダリングする必要がある場合)。

反応フックがクリーンアップコードを実行する前に余分なことを行うかどうかはわかりませんが、アニメーションキャンセル部分はうまく機能しません。useEffectフックは状態変数をロックするクロージャーのようなことをしますか?

UseEffectの実行順序とその内部クリーンアップロジックは何ですか?以下に記述するコードに何か問題があり、cancelAnimationFrameが完全に機能しませんか?

ありがとう。

//import React, { useState, useEffect } from "react";

const {useState, useEffect} = React;

//import ReactDOM from "react-dom";

function App() {
  const [startSeconds, setStartSeconds] = useState(Math.random());
  const [progress, setProgress] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => {
      setStartSeconds(Math.random());
    }, 1000);

    return () => clearInterval(interval);
  }, []);

  useEffect(
    () => {
      let raf = null;

      const onFrame = () => {
        const currentProgress = startSeconds / 120.0;
        setProgress(Math.random());
        // console.log(currentProgress);
        loopRaf();
        if (currentProgress > 100) {
          stopRaf();
        }
      };

      const loopRaf = () => {
        raf = window.requestAnimationFrame(onFrame);
        // console.log('Assigned Raf ID: ', raf);
      };

      const stopRaf = () => {
        console.log("stopped", raf);
        window.cancelAnimationFrame(raf);
      };

      loopRaf();

      return () => {
        console.log("Cleaned Raf ID: ", raf);
        // console.log('init', raf);
        // setTimeout(() => console.log("500ms later", raf), 500);
        // setTimeout(()=> console.log('5s later', raf), 5000);
        stopRaf();
      };
    },
    [startSeconds]
  );

  let t = [];
  for (let i = 0; i < 1000; i++) {
    t.Push(i);
  }

  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <text>{progress}</text>
      {t.map(e => (
        <span>{progress}</span>
      ))}
    </div>
  );
}

ReactDOM.render(<App />,
document.querySelector("#root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.7.0-alpha.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.7.0-alpha.2/umd/react-dom.production.min.js"></script>
<div id="root"></div>
9
hijiangtao

これらの3行のコードをコンポーネントに入れると、優先順位が表示されます。

  useEffect(() => {
    console.log('useEffect')
    return () => {
      console.log('useEffect cleanup')
    }
  })

  window.requestAnimationFrame(() => console.log('requestAnimationFrame'))

  useLayoutEffect(() => {
    console.log('useLayoutEffect')
    return () => {
      console.log('useLayoutEffect cleanup')
    }
  })

useLayoutEffect > requestAnimationFrame > useEffect

発生している問題は、loopRafのクリーンアップ関数が実行される前に別のアニメーションフレームを要求するuseEffectが原因です。

さらなるテストにより、useLayoutEffectは常にrequestAnimationFrameの前に呼び出され、そのクリーンアップ関数は重複を防ぐ次の実行の前に呼び出されることが示されました。

useEffectuseLayoutEffectに変更すると、問題が解決するはずです。

useEffectuseLayoutEffectは、useStateの呼び出しと同じようなタイプの場合、コードに現れる順序で呼び出されます。

これを確認するには、次の行を実行します。

  useEffect(() => {
    console.log('useEffect-1')
  })
  useEffect(() => {
    console.log('useEffect-2')
  })
  useLayoutEffect(() => {
    console.log('useLayoutEffect-1')
  })
  useLayoutEffect(() => {
    console.log('useLayoutEffect-2')
  })
3
Kyle Richardson

上記の答えで明確ではないことの1つは、ミックスに複数のコンポーネントがある場合にエフェクトが実行される順序です。私たちはuseContextを介して親とその子の間の調整を伴う作業を行ってきたので、順序は私たちにとってより重要です。 useLayoutEffectuseEffectは、この点で異なる方法で機能します。

useEffectは、クリーンアップと新しい効果を実行してから、次のコンポーネント(深さ優先)に移動して同じことを行います。

useLayoutEffectは各コンポーネントのクリーンアップを実行し(深さ優先)、次にすべてのコンポーネントの新しい効果を実行します(深さ優先)。

render parent
render a
render b
layout cleanup a
layout cleanup b
layout cleanup parent
layout effect a
layout effect b
layout effect parent
effect cleanup a
effect a
effect cleanup b
effect b
effect cleanup parent
effect parent
const Test = (props) => {
  const [s, setS] = useState(1)

  console.log(`render ${props.name}`)

  useEffect(() => {
    const name = props.name
    console.log(`effect ${props.name}`)
    return () => console.log(`effect cleanup ${name}`)
  })

  useLayoutEffect(() => {
    const name = props.name
    console.log(`layout effect ${props.name}`)
    return () => console.log(`layout cleanup ${name}`)
  })

  return (
    <>
      <button onClick={() => setS(s+1)}>update {s}</button>
      <Child name="a" />
      <Child name="b" />
    </>
  )
}

const Child = (props) => {
  console.log(`render ${props.name}`)

  useEffect(() => {
    const name = props.name
    console.log(`effect ${props.name}`)
    return () => console.log(`effect cleanup ${name}`)
  })

  useLayoutEffect(() => {
    const name = props.name
    console.log(`layout effect ${props.name}`)
    return () => console.log(`layout cleanup ${name}`)
  })

  return <></>
}
5
Aidan Kane

フックを操作し、ライフサイクル機能を実装しようとするときに目を向ける必要がある2つの異なるフックがあります。

ドキュメントに従って:

useEffectは、reactがコンポーネントをレンダリングした後に実行され、エフェクトコールバックがブラウザのペイントをブロックしないようにします。これは、componentDidMountcomponentDidUpdateがレンダリング後に同期して実行されるクラスコンポーネントの動作とは異なります。

したがって、これらのライフサイクルでrequestAnimationFrameを使用することは見かけ上は機能しますが、useEffectでわずかな不具合があります。そのため、useEffectは、応答の受信後にDOMの変更につながるAPI呼び出しを行うなど、視覚的な更新をブロックする必要がない場合に使用する必要があります。

あまり一般的ではないが、視覚的なDOM更新を処理する際に非常に便利な別のフックはuseLayoutEffectです。 ドキュメントによる

シグニチャーはuseEffectと同じですが、すべてのDOMの変更後に同期して起動します。これを使用して、DOMからレイアウトを読み取り、同期的に再レン​​ダリングします。 useLayoutEffect内でスケジュールされた更新は、ブラウザーがペイントする前に同期的にフラッシュされます。

したがって、エフェクトがDOMを(DOMノードrefを介して)変化させ、DOMの変化がレンダリングされてからエフェクトが変化するまでの間にDOMノードの外観を変更する場合、useEffectを使用したくない場合。 useLayoutEffectを使用します。そうしないと、DOMの変更が有効になったときに、requestAnimationFrameの場合とまったく同じように、ユーザーにちらつきが見える可能性があります

//import React, { useState, useEffect } from "react";

const {useState, useLayoutEffect} = React;

//import ReactDOM from "react-dom";

function App() {
  const [startSeconds, setStartSeconds] = useState("");
  const [progress, setProgress] = useState(0);

  useLayoutEffect(() => {
    setStartSeconds(Math.random());

    const interval = setInterval(() => {
      setStartSeconds(Math.random());
    }, 1000);

    return () => clearInterval(interval);
  }, []);

  useLayoutEffect(
    () => {
      let raf = null;

      const onFrame = () => {
        const currentProgress = startSeconds / 120.0;
        setProgress(Math.random());
        // console.log(currentProgress);
        loopRaf();
        if (currentProgress > 100) {
          stopRaf();
        }
      };

      const loopRaf = () => {
        raf = window.requestAnimationFrame(onFrame);
        // console.log('Assigned Raf ID: ', raf);
      };

      const stopRaf = () => {
        console.log("stopped", raf);
        window.cancelAnimationFrame(raf);
      };

      loopRaf();

      return () => {
        console.log("Cleaned Raf ID: ", raf);
        // console.log('init', raf);
        // setTimeout(() => console.log("500ms later", raf), 500);
        // setTimeout(()=> console.log('5s later', raf), 5000);
        stopRaf();
      };
    },
    [startSeconds]
  );

  let t = [];
  for (let i = 0; i < 1000; i++) {
    t.Push(i);
  }

  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <text>{progress}</text>
      {t.map(e => (
        <span>{progress}</span>
      ))}
    </div>
  );
}

ReactDOM.render(<App />,
document.querySelector("#root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.7.0-alpha.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.7.0-alpha.2/umd/react-dom.production.min.js"></script>
<div id="root"></div>
3
Shubham Khatri