『useEffect完全ガイド』ノート


本編は個人のノートで、教程ではありませんて、原文を読むことを提案して、比較的に長いですが、2、3回見て試してみるとやはりとても向上して、問題があって一緒に交流することができます~
原文住所:https://overreacted.io/zh-hans/a-complete-guide-to-useeffect/作者紹介:https://overreacted.io/zh-hans/my-decade-in-review/

useEffect(fn,[])とcomponentDidMountの違い


前者はpropsとstateをキャプチャするので,コールバック関数でどのくらい待っても得られるpropsとstateは初期値(タイマ例)である.リアルタイムデータを取得する必要がある場合は、refまたは後述する他の方法を使用します.

関数の保存に最適な場所

  • はstateとpropsに関係なく、コンポーネントの外に
  • 配置される.
  • はあるuseEffectと関係があるだけで、この中に
  • を入れます.
  • effectは以上の2つ以外の関数を使用してuseCallbackでラップします.

  • レンダーごと(Per Render)

  • stateとpropsはフレームごとに完全に独立しており、今回のレンダリングではオブジェクトタイプを含む定数です.もちろん、setStateを放棄してオブジェクトの属性関数を直接変更することはできません.フレームごとに独立してレンダリングされます.呼び出されたときに入力されるパラメータは、コンポーネントの更新に伴って変化しません.
  • には独自のEffectsがあります.つまり、Effectsの内部には、セカンダリレンダリング時のstateとpropsが保存されています.effectsはコンポーネントレンダリングの一部と理解できます.

  • effects運転タイミング


    Reactはブラウザの描画後にeffectsを実行するだけです.

    遅延の場合

  • effectに遅延がある場合、実行時に出力されるstateおよびpropsは、コンポーネントの更新に伴って変化しない当初の呼び出し時の値である(原理は閉パケット).しかし、classコンポーネントのdidUpdateでは随時更新されています.すなわち、以下の2つの表記は等価
  • である.
    function Example(props) {
         
     useEffect(() => {
         
       setTimeout(() => {
         
         console.log(props.counter);
       }, 1000);
     });
     // ...
    }
    function Example(props) {
         
     const counter = props.counter;
       useEffect(() => {
         
           setTimeout(() => {
         
             console.log(counter);
           }, 1000);
     });
     // ...
    }
    

    useRefはeffectでリアルタイムのstateとeffectを使用します


    次の実装はclassでの動作をシミュレートすることに相当します.
    function Example() {
         
     const [count, setCount] = useState(0);
     const latestCount = useRef(count);
     useEffect(() => {
         
          // Set the mutable latest value
          latestCount.current = count; 
          setTimeout(() => {
         
          // Read the mutable latest value 
              console.log(`You clicked ${
           latestCount.current} times`); 
         }, 3000);
     });
    // ...
    

    effectsでのクリア


    effectsのreturn:ブラウザがレンダリングされるたびにeffectsを実行し、まずreturnで定義された関数を呼び出してクリア作業を行いますが、関数のstateまたはpropsは前回の値、すなわち定義時に保存されたデータです.このidを1つずつ購読してクリアすると仮定し、1回目のidが10、2回目のidが20の場合、reactの実行順序は次のようになります.
  • idが10のeffect
  • をクリア
  • レンダリングid 20のUI
  • idが20のeffect
  • を実行する
    実際には
  • レンダリングid 20のUI
  • idが10のeffect
  • をクリア
  • ブラウザで描画します.画面に{id:20}のUIが表示されます.
  • idが20のeffect
  • を実行する
    つまり、このフレームをレンダリングした後、前のフレームをクリアします.

    useEffect更新条件


    useEffectに依存配列パラメータ(deps)を提供するのは、reactというeffectがこれらのパラメータしか使用されていないことをreactに伝えることに相当し、コンポーネントの更新時にすべてのパラメータが変化しない場合、reactは今回のeffectの実行をスキップします.パラメータが変更された場合は、すべてのパラメータが同期されます.更新条件はどのように書きますか?
    「effect依存を誠実に伝えることをハードルールとし、依存をリストする」
  • の第1のポリシーは、すべてのeffectで使用されるコンポーネント内の値を依存に含めることである.
  • の第2のポリシーは、effect内部のコードを変更して、必要に応じて値が変更されるだけであることを確認することです.

  • setStateの関数形式


    前の状態に基づいて状態を更新したい場合はsetStateの関数形式を使用できます.
    useEffect(() => {
         
        const id = setInterval(() => {
         
              setCount(c => c + 1);
        }, 1000);
        return () => clearInterval(id);
    }, []);
    

    useReducer


    1つの原則:effectで最小の情報のみを伝達する.上記の方法では、stepが変更されるとcountがステップ長を更新し、タイマがクリアされると再起動するなど、問題を完全に解決できない場合があります.
    function Counter() {
         
      const [count, setCount] = useState(0);
      const [step, setStep] = useState(1);
    
      useEffect(() => {
         
        const id = setInterval(() => {
         
          setCount(c => c + step);
        }, 1000);
        return () => clearInterval(id);
      }, [step]);
      return (
        <>
          <h1>{
         count}</h1>
          <input value={
         step} onChange={
         e => setStep(Number(e.target.value))} />
        </>
      );
    }
    

    タイマーを再起動しない場合はuseReducerが必要です.
    あるステータスを更新し、このステータスの更新が別のステータスの値に依存する場合は、useReducerで置き換える必要があります.setSomething(something=>...)のようなコードを書くと、reducerを使うことを考えるきっかけになるかもしれません.reducerは、コンポーネント内で何が起こったのか(actions)とステータスがどのように応答して更新されるかを別々に記述することができます.
    effectのstep依存をdispatch依存で置き換えた.
    import React, {
          useReducer, useEffect } from 'react';
    const initState = {
         
        count: 0,
        step: 1
    }
    const reducer = (state, action) => {
         
        const {
          count, step } = state
        if (action.type === 'tick') {
         
            return {
          count: count + step, step }
        } else if (action.type === 'step') {
         
            return {
          count, step: action.step }
        }
    }
    const Reducer01 = () => {
         
        const [state, dispatch] = useReducer(reducer, initState)
        const {
         count, step} = state
    
        useEffect(() => {
         
            const t = setInterval(() => {
         
                dispatch({
          type: 'tick' })
            }, 1000)
            return ()=>[
                clearInterval(t)
            ]
        }, [dispatch]) //  , , dispatch 
        
        return (
            <div>
                <h3> reducer</h3>
                <p>{
         count}</p>
                <input onChange={
         e=>dispatch({
         type:'step',step:Number(e.target.value)})} />
            </div>
        );
    }
    
    export default Reducer01;
    

    propsからのstep


    上記の例のstepがpropsから来た場合、reducerをコンポーネント内に書き込んでstepを取得することができる.しかし、このモードはいくつかの最適化を無効にするので、reducerはコンポーネントレンダリングのたびに新しいものを生成することを避けるべきです.useReducerはHooksの「カンニングモード」と理解できる.更新ロジックと記述を区別することができます.利点は、不要な依存を除去し、不要なeffect呼び出しを避けるのに役立つことです.
    const Reducer02 = ({
          step }) => {
         
        const reducer = (state, action) => {
         
           const {
          count } = state
            if (action.type === 'tick') {
         
                return {
          count: count + step, step }
            } else if (action.type === 'step') {
         
                return {
          count, step: action.step }
            }
        }
        
        const [state, dispatch] = useReducer(reducer, initState)
        const {
          count } = state
        
        useEffect(() => {
         
            const t = setInterval(() => {
         
                dispatch({
          type: 'tick' })
            }, 1000)
            return () => [
                clearInterval(t)
            ]
        }, [])
        return (……);
    }
    

    データの取得


    クエリー・パラメータに基づいて結果を自動的に取得


    取得データの一例:クエリー・パラメータqueryが変更されると、useEffectはデータのプル操作を自動的に実行します.
    function SearchResults() {
         
      const [query, setQuery] = useState('react');
    
      useEffect(() => {
         
        function getFetchUrl() {
         
          return 'https://hn.algolia.com/api/v1/search?query=' + query;    
        }
    
        async function fetchData() {
         
          const result = await axios(getFetchUrl());
          setData(result.data);
        }
    
        fetchData();
      }, [query]); // ✅ Deps are OK
      // ...
    }
    

    クエリー・アドレスの多重取得方法


    上記の例でgetFetchUrlロジックを多重化する必要がある場合、それをuseEffectの外側のコンポーネントに書き、関数を依存として使用すると、リフレッシュのたびにデータが要求されます.
    function SearchResults() {
         
      //  Re-triggers all effects on every render  
      function getFetchUrl(query) {
          
         return 'https://hn.algolia.com/api/v1/search?query=' + query; 
      }
      useEffect(() => {
         
        const url = getFetchUrl('react');
        // ... Fetch data and do something ...
      }, [getFetchUrl]); //  Deps are correct but they change too often
    
      useEffect(() => {
         
        const url = getFetchUrl('redux');
        // ... Fetch data and do something ...
      }, [getFetchUrl]); //  Deps are correct but they change too often
    
      // ...
    }
    

    ソリューションの説明は次のとおりです。

  • は、この関数がコンポーネント内の値
  • を使用しないことを前提として、関数をコンポーネントの外部に書く.
    function getFetchUrl(query) {
         
      return 'https://hn.algolia.com/api/v1/search?query=' + query;
    }
    function SearchResults() {
         
      useEffect(() => {
         
        const url = getFetchUrl('react');
        // ... Fetch data and do something ...
      }, []); // ✅ Deps are OK
    
      useEffect(() => {
         
        const url = getFetchUrl('redux');
        // ... Fetch data and do something ...
      }, []); // ✅ Deps are OK
      // ...
    }
    
  • useCallbackを使用し、関数を依存として使用します.
  • function SearchResults() {
         
      // ✅ Preserves identity when its own deps are the same  
      const getFetchUrl = useCallback((query) => {
         
          return 'https://hn.algolia.com/api/v1/search?query=' + query;  
      }, []);  // ✅ Callback deps are OK
      useEffect(() => {
         
        const url = getFetchUrl('react');
        // ... Fetch data and do something ...
      }, [getFetchUrl]); // ✅ Effect deps are OK
    
      useEffect(() => {
         
        const url = getFetchUrl('redux');
        // ... Fetch data and do something ...
      }, [getFetchUrl]); // ✅ Effect deps are OK
    
      // ...
    }
    
  • queryが他の場所で変更可能なコンポーネント内stateである場合、
  • に依存するように設定することができる.
    function SearchResults() {
         
      const [query, setQuery] = useState('react');
    
      // ✅ Preserves identity until query changes
      const getFetchUrl = useCallback(() => {
         
         return 'https://hn.algolia.com/api/v1/search?query=' + query;  
      }, [query]);  // ✅ Callback deps are OK
      useEffect(() => {
         
        const url = getFetchUrl();
        // ... Fetch data and do something ...
      }, [getFetchUrl]); // ✅ Effect deps are OK
    
      // ...
    }
    
  • 関数が親コンポーネントから入力場合、例えば、子コンポーネントが親コンポーネントのquery変更を発見した後にデータを要求する場合、上記の方法
  • に依然として適用される.
    function Parent() {
         
      const [query, setQuery] = useState('react');
    
      // ✅ Preserves identity until query changes
      const fetchData = useCallback(() => {
         
          const url = 'https://hn.algolia.com/api/v1/search?query=' + query;    // ... Fetch data and return it ...
      }, [query]);  // ✅ Callback deps are OK
      
      return <Child fetchData={
         fetchData} />
    }
    
    function Child({
          fetchData }) {
         
      let [data, setData] = useState(null);
    
      useEffect(() => {
         
        fetchData().then(setData);
      }, [fetchData]); // ✅ Effect deps are OK
    
      // ...
    }
    

    関数はライフサイクル全体で1部しかなく、アドレスは永遠に変わらず、query変動後のリフレッシュを駆動できないため、classコンポーネントには使用できません.解法は,サブコンポーネントをカプセル化してデータを要求し,queryをサブコンポーネントに渡し,サブコンポーネントでqueryを判断して更新するか否かを決定する.この場合queryは,サブコンポーネントをdiffするためにのみ伝達されることに相当し,hooksではeffectがデータストリームに関与し,この問題をよりよく解決できる.
  • と同様に、useMemoを使用すると複雑なオブジェクトに対してより多くのことをすることができ、次の例ではuseMemoを使用してdomに必要なstlyeフォーマットをカプセル化し、colorが変更されるとdomはレンダリングに従います.
  • function ColorPicker() {
         
      // Doesn't break Child's shallow equality prop check
      // unless the color actually changes.
      const [color, setColor] = useState('pink');
      const style = useMemo(() => ({
          color }), [color]);
      return <Child style={
         style} />;
    }
    
  • 注意すべき点は、著者は「useCallbackをあちこちで使用するのは不器用なことだ.上記の例では、fetchDataを私のeffectに置く傾向がある(カスタムHookに抽出できる)か、トップダウンから導入する.effectsを簡単に維持し、コールバックを呼び出すと複雑になる」ということだ.

  • 競合問題


    定義:「要求結果が一致する順序は保証されません.たとえば、私が{id:10}を要求したときのデータが{id:20}に更新されますが、{id:20}の要求が先に返されます.要求が早くても遅く戻った場合、ステータス値が誤って上書きされます.」解決策は、ブール値を使用してステータスを追跡することです.
    function Article({
          id }) {
         
      const [article, setArticle] = useState(null);
    
      useEffect(() => {
         
        let didCancel = false;
        async function fetchData() {
         
          //  ,
          //  effect didCancel true,
          //  , 
          const article = await API.fetchArticle(id);
          if (!didCancel) {
         
              setArticle(article);
          }
        }
    
        fetchData();
    
        return () => {
         
              didCancel = true;
        };
      }, [id]);
    
      // ...
    }
    

    の最後の部分


    本稿で説明するeffectは基本的に初級使用レベルであり、コミュニティはeffectベースのhooksを発売し、頻繁に手動でeffectを作成することを減らします.