useEffect完全指南筆記

1. 渲染過程

第一次:

  • React: 給我狀態為0時候的UI
  • 你的組件
  1. 給你需要渲染的內容:
  <p>You clicked 0 times</p>。
  1. 記得在渲染之后調用這個effect:
() => { document.title = 'You clicked 0 times' }。
  • React: 沒問題。開始更新UI,喂瀏覽器,我要給DOM添加一些東西。
  • Browser: 酷,我已經把它繪制到屏幕上啦。
  • React:好的,我要開始運行我的effect啦:
    1. 運行
() => { document.title = 'You clicked 0 times' }。

點擊之后:

  • 你的組件: 喂 React,把我的狀態設置為1。
  • React: 給我狀態為1的UI。
  • 你的組件
  1. 給你需要渲染的內容:
<p>You clicked 1 times</p>。

2.記得在渲染完成之后,調用這個effect:

() => { document.title = 'You clicked 1 times' }。
  • React: 沒問題。開始更新UI,喂瀏覽器,我修改了DOM。
  • Browser:酷,我已經將更改繪制到屏幕上啦。
  • React:好的,我現在開始運行屬于這次渲染的effect:
    1. 運行
() => { document.title = 'You clicked 1 times' }。

2. 逆潮而動

到目前為止,我們可以明確地喊出下面重要的事實: 每一個組件內的函數(包括事件處理函數,effects,定時器或者API調用等等)會捕獲某次渲染中定義的propsstate。

在組件內什么時候去讀取propsstate是無關緊要的。因為他們不會改變。在單次渲染的范圍內,propsstate始終保持不變。(解構賦值props使得這一點更明顯。)

當然,有的時候你可能想在effect的回調函數里讀取最新的值而不是捕獲的值。最簡單的實現方法就是refs
這篇文章的最后一部分介紹了相關內容。

需要注意的是當你想要從過去渲染中的函數里讀取未來propsstate,你是在逆潮而動。雖然它并沒有(有時候可能也需要這樣做),但它因為打破了默認范式會使代碼顯得不夠“干凈”。這是Hooks有意而為之的,因為它能幫助突出哪些代碼是脆弱的,是需要依賴時間次序的。在class中,如果發生這種情況就沒那么顯而易見了。

下面這個計數器版本模擬了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);
  });
  // ...

3. Effect中的清理

文檔中解釋的,有些effects可能需要有一個清理步驟。本質上,它的目的是消除副作用(effect),比如取消訂閱。

  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(props.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.id, handleStatusChange);
    };
  });

假設第一次渲染的時候props{id: 10},第二次渲染的時候是{id: 20}。你可能會認為發生了下面的這些事:

  • React清除了{id: 10}的effect。
  • React渲染{id: 20}的UI。
  • React運行{id: 20}的effect。
    (事實并不是這樣。)

React只會在瀏覽器繪制后運行effects。這使得你的應用更流暢,因為大多數effects并不會阻塞屏幕的更新。effect的清除同樣被延遲了。上一次的effect會在重新渲染后被清除:

  • React渲染{id: 20}的UI。
  • 瀏覽器繪制。我們在屏幕上看到{id: 20}的UI。
  • React清除{id: 10}的effect。
  • React運行{id: 20}的effect。

effect的清除并不會讀取“最新”的props。它只能讀取到定義它的那次渲染中的props值:

// First render, props are {id: 10}
function Example() {
  // ...
  useEffect(
    // Effect from first render
    () => {
      ChatAPI.subscribeToFriendStatus(10, handleStatusChange);
      // Cleanup for effect from first render
      return () => {
        ChatAPI.unsubscribeFromFriendStatus(10, handleStatusChange);
      };
    }
  );
  // ...
}

// Next render, props are {id: 20}
function Example() {
  // ...
  useEffect(
    // Effect from second render
    () => {
      ChatAPI.subscribeToFriendStatus(20, handleStatusChange);
      // Cleanup for effect from second render
      return () => {
        ChatAPI.unsubscribeFromFriendStatus(20, handleStatusChange);
      };
    }
  );
  // ...
}

王國會崛起轉而復歸塵土,太陽會脫落外層變為白矮星,最后的文明也遲早會結束。但是第一次渲染中effect的清除函數只能看到{id: 10}這個props。

這正是為什么React能做到在繪制后立即處理effects — 并且默認情況下使你的應用運行更流暢。如果你的代碼需要依然可以訪問到老的props。

4. 告訴React去對比你的Effects

React并不能猜測到函數做了什么如果不先調用的話。
如果想要避免effects不必要的重復調用,你可以提供給useEffect一個依賴數組參數(deps):

  useEffect(() => {
    document.title = 'Hello, ' + name;
  }, [name]); // Our deps

這好比你告訴React:“Hey,我知道你看不到這個函數里的東西,但我可以保證只使用了渲染中的name,別無其他。”
如果當前渲染中的這些依賴項和上一次運行這個effect的時候值一樣,因為沒有什么需要同步React會自動跳過這次effect:

const oldEffect = () => { document.title = 'Hello, Dan'; };
const oldDeps = ['Dan'];

const newEffect = () => { document.title = 'Hello, Dan'; };
const newDeps = ['Dan'];

// React can't peek inside of functions, but it can compare deps.
// Since all deps are the same, it doesn’t need to run the new effect.

5. 兩種誠實告知依賴的方法

有兩種誠實告知依賴的策略。你應該從第一種開始,然后在需要的時候應用第二種。
第一種策略是在依賴中包含所有effect中用到的組件內的值。讓我們在依賴中包含count:

useEffect(() => {
  const id = setInterval(() => {
    setCount(count + 1);
  }, 1000);
  return () => clearInterval(id);
}, [count]);

現在依賴數組正確了。雖然它可能不是太理想但確實解決了上面的問題?,F在,每次count修改都會重新運行effect,并且定時器中的setCount(count + 1)會正確引用某次渲染中的 count值:

// First render, state is 0
function Counter() {
  // ...
  useEffect(
    // Effect from first render
    () => {
      const id = setInterval(() => {
        setCount(0 + 1); // setCount(count + 1)
      }, 1000);
      return () => clearInterval(id);
    },
    [0] // [count]
  );
  // ...
}

// Second render, state is 1
function Counter() {
  // ...
  useEffect(
    // Effect from second render
    () => {
      const id = setInterval(() => {
        setCount(1 + 1); // setCount(count + 1)
      }, 1000);
      return () => clearInterval(id);
    },
    [1] // [count]
  );
  // ...
}

這能解決問題但是我們的定時器會在每一次count改變后清除和重新設定。這應該不是我們想要的結果:(依賴發生了變更,所以會重新運行effect。)

第二種策略是修改effect內部的代碼以確保它包含的值只會在需要的時候發生變更。
我們不想告知錯誤的依賴--我們只是修改effect使得依賴更少。

6. 移除依賴的常用技巧。

  • 讓Effects自給自足

當我們想要根據前一個狀態更新狀態的時候,我們可以使用setState函數形式

useEffect(() => {
  const id = setInterval(() => {
    setCount(c => c + 1);
  }, 1000);
  return () => clearInterval(id)
},[])

我喜歡把類似這種情況稱為“錯誤的依賴”。是的,因為我們在effect中寫了setCount(count + 1)所以count是一個必需的依賴。但是,我們真正想要的是把count轉換為count+1,然后返回給React??墒荝eact其實已經知道當前的count我們需要告知React的僅僅是去遞增狀態 - 不管它現在具體是什么值。

這正是setCount(c => c + 1)做的事情。你可以認為它是在給React“發送指令”告知如何更新狀態。這種“更新形式”在其他情況下也有幫助,比如你需要批量更新

注意我們做到了移除依賴,并且沒有撒謊。我們的effect不再讀取渲染中的count值。

盡管effect只運行了一次,第一次渲染中的定時器回調函數可以完美地在每次觸發的時候給React發送c => c + 1更新指令。它不再需要知道當前的count值。因為React已經知道了。

  • 函數式更新

只在effects中傳遞最小的信息會很有幫助。類似于setCount(c => c + 1)這樣的更新形式比setCount(count + 1)傳遞了更少的信息,因為它不再被當前的count值“污染”。它只是表達了一種行為(“遞增”)。“Thinking in React”也討論了如何找到最小狀態。原則是類似的,只不過現在關注的是如何更新。

表達意圖(而不是結果)和Google Docs如何處理共同編輯異曲同工。雖然這個類比略微延伸了一點,函數式更新在React中扮演了類似的角色。它們確保能以批量地和可預測的方式來處理各種源頭(事件處理函數,effect中的訂閱,等等)的狀態更新。

然而,即使是setCount(c => c + 1)也并不完美。它看起來有點怪,并且非常受限于它能做的事。舉個例子,如果我們有兩個互相依賴的狀態,或者我們想基于一個prop來計算下一次的state,它并不能做到。幸運的是, setCount(c => c + 1)有一個更強大的姐妹模式,它的名字叫useReducer。

  • 解耦來自Actions的更新

我們來修改上面的例子讓它包含兩個狀態:countstep。我們的定時器會每次在count上增加一個step值:

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))} />
    </>
  );
}

注意我們沒有撒謊。既然我們在effect里使用了step,我們就把它加到依賴里。所以這也是為什么代碼能運行正確。

當你想更新一個狀態,并且這個狀態更新依賴于另一個狀態的值時,你可能需要用useReducer去替換它們。
當你寫類似setSomething(something => ...)這種代碼的時候,也許就是考慮使用reducer的契機。reducer可以讓你把組件內發生了什么(actions)和狀態如何響應并更新分開表述。

我們用一個dispatch依賴去替換effect的step依賴:

const [state, dispatch] = useReducer(reducer, initialState);
const {count, step} = state;

useEffect(() => {
  const id = setInterval(() => {
    dispatch({type: 'tick'}); // Instead of setCount(c => c + step);
  }, 1000);
  return () => clearInterval(id)
}, [dispatch]);

 return (
    <>
      <h1>{count}</h1>
      <input value={step} onChange={e => {
        dispatch({
          type: 'step',
          step: Number(e.target.value)
        });
      }} />
    </>
  );

React會保證dispatch在組件的聲明周期內保持不變,所以上面例子中不再需要重新訂閱定時器。

(你可以從依賴中去除dispatch, setState, 和useRef包裹的值因為React會確保它們是靜態的。不過你設置了它們作為依賴也沒什么問題。)

相比于直接在effect里面讀取狀態。它dispatch了一個action來描述發生了什么。這使得我們的effect和step狀態解耦。我們的effect不再關心怎么更新狀態,它只負責告訴我們發生了什么。更新的邏輯全都交給reducer去統一處理:

const initialState = {
  count: 0,
  step: 1,
};

function 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 };
  } else {
    throw new Error();
  }
}
  • 為什么useReducer是Hooks的作弊模式(依賴props去計算下一個狀態)

我們已經學習到如何移除effect的依賴。不管狀態更新是依賴上一個形態還是依賴另一個狀態。但假如我們需要依賴props去計算下一個狀態呢?
舉個例子,也許我們的API是<Counter step={1} />。
實際上, 我們可以避免依賴props.step,我們可以把reducer函數放到組件內去讀取props:

function Counter({ step }) {
  const [count, dispatch] = useReducer(reducer, 0);

  function reducer(state, action) {
    if (action.type === 'tick') {
      return state + step;
    } else {
      throw new Error();
    }
  }

  useEffect(() => {
    const id = setInterval(() => {
      dispatch({ type: 'tick' });
    }, 1000);
    return () => clearInterval(id);
  }, [dispatch]);

  return <h1>{count}</h1>;
}

這種模式會使一些優化失效,所以你應該避免濫用它。不過如果你需要你完全可以在reducer里面訪問props。

即使在這個例子中,React也保證dispatch在每次渲染中都是一樣的。所以你可以在以來中去掉它。它不會引起effect不必要的重復執行。
你可能會疑惑:這怎么可能?在之前渲染中調用的reducer怎么“知道”新的props?答案是當你dispatch的時候,React只是記住了action - 它會在下一次渲染中再次調用reducer。在那個時候,新的props就可以被訪問到,而且reducer調用也不是在effect里。

這就是為什么我傾向認為useReducer是Hooks的“作弊模式”。它可以把更新邏輯和描述發生了什么分開。結果是,這可以幫助我移除不必需的依賴,避免不必要的effect調用。

7. 把函數移到Effects里

function SearchResults() {
  const [query, setQuery] = useState('react');

  // Imagine this function is also long
  function getFetchUrl() {
    return 'https://hn.algolia.com/api/v1/search?query=' + query;
  }

  // Imagine this function is also long
  async function fetchData() {
    const result = await axios(getFetchUrl());
    setData(result.data);
  }

  useEffect(() => {
    fetchData();
  }, []);

  // ...
}

如果我們忘記去更新使用這些函數(很有可能通過其他函數調用)的effects的依賴,我們的effects就不會同步到props和state帶來的變更。這當然不是我們想要的。
幸運的是,對于這個問題有一個簡單的解決方案。如果某些函數僅在effect中調用,你可以把它們的定義移到effect中:

function SearchResults() {
  // ...
  useEffect(() => {
    // We moved these functions inside!
    function getFetchUrl() {
      return 'https://hn.algolia.com/api/v1/search?query=react';
    }
    async function fetchData() {
      const result = await axios(getFetchUrl());
      setData(result.data);
    }

    fetchData();
  }, []); // ? Deps are OK
  // ...
}

這么做有什么好處呢?我們不再需要去考慮這些“間接依賴”。我們的依賴數組也不再撒謊:在我們的effect中確實沒有再使用組件范圍內的任何東西。

useEffect的設計意圖就是要強迫你關注數據流的改變,然后決定我們的effects該如何和它同步 - 而不是忽視它直到我們的用戶遇到了bug。

8.不能把這個函數放到Effect里

有時候你可能不想把函數移入effect里。比如,組件內有幾個effect使用了相同的函數,你不想在每個effect里復制黏貼一遍這個邏輯。也或許這個函數是一個prop。
effects不應該對它的依賴撒謊。
函數每次渲染都會改變這個事實本身就是個問題。比如有兩個effects會調用 getFetchUrl:

function SearchResults() {
  function getFetchUrl(query) {
    return 'https://hn.algolia.com/api/v1/search?query=' + query;
  }

  useEffect(() => {
    const url = getFetchUrl('react');
    // ... Fetch data and do something ...
  }, []); // ?? Missing dep: getFetchUrl

  useEffect(() => {
    const url = getFetchUrl('redux');
    // ... Fetch data and do something ...
  }, []); // ?? Missing dep: getFetchUrl

  // ...
}

在這個例子中,你可能不想把getFetchUrl 移到effects中,因為你想復用邏輯。

另一方面,如果你對依賴很“誠實”,你可能會掉到陷阱里。我們的兩個effects都依賴getFetchUrl,而它每次渲染都不同,所以我們的依賴數組會變得無用:

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

  // ...
}

一個可能的解決辦法是把getFetchUrl從依賴中去掉。但是,我不認為這是好的解決方式。這會使我們后面對數據流的改變很難被發現從而忘記去處理。這會導致類似于上面“定時器不更新值”的問題。

相反的,我們有兩個更簡單的解決辦法。

第一個, 如果一個函數沒有使用組件內的任何值,你應該把它提到組件外面去定義,然后就可以自由地在effects中使用:

// ? Not affected by the data flow
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

  // ...
}

你不再需要把它設為依賴,因為它們不在渲染范圍內,因此不會被數據流影響。它不可能突然意外地依賴于props或state。

或者, 你也可以把它包裝成 useCallback Hook:

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

  // ...
}

useCallback本質上是添加了一層依賴檢查。它以另一種方式解決了問題 - 我們使函數本身只在需要的時候才改變,而不是去掉對函數的依賴。

我們來看看為什么這種方式是有用的。之前,我們的例子中展示了兩種搜索結果(查詢條件分別為'react''redux')。但如果我們想添加一個輸入框允許你輸入任意的查詢條件(query)。不同于傳遞query參數的方式,現在getFetchUrl會從狀態中讀取。

我們很快發現它遺漏了query依賴:

function SearchResults() {
  const [query, setQuery] = useState('react');
  const getFetchUrl = useCallback(() => { // No query argument
    return 'https://hn.algolia.com/api/v1/search?query=' + query;
  }, []); // ?? Missing dep: query
  // ...
}

如果我把query添加到useCallback 的依賴中,任何調用了getFetchUrl的effect在query改變后都會重新運行:

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

  // ...
}

我們要感謝useCallback,因為如果query 保持不變,getFetchUrl也會保持不變,我們的effect也不會重新運行。但是如果query修改了,getFetchUrl也會隨之改變,因此會重新請求數據。這就像你在Excel里修改了一個單元格的值,另一個使用它的單元格會自動重新計算一樣。

這正是擁抱數據流和同步思維的結果。對于通過屬性從父組件傳入的函數這個方法也適用:

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

  // ...
}

因為fetchData只有在Parentquery狀態變更時才會改變,所以我們的Child只會在需要的時候才去重新請求數據。

9.函數是數據流的一部分嗎?

有趣的是,這種模式在class組件中行不通,并且這種行不通恰到好處地揭示了effect和生命周期范式之間的區別。考慮下面的轉換:

class Parent extends Component {
  state = {
    query: 'react'
  };
  fetchData = () => {
    const url = 'https://hn.algolia.com/api/v1/search?query=' + this.state.query;
    // ... Fetch data and do something ...
  };
  render() {
    return <Child fetchData={this.fetchData} />;
  }
}

class Child extends Component {
  state = {
    data: null
  };
  componentDidMount() {
    this.props.fetchData();
  }
  render() {
    // ...
  }
}

你可能會想:“少來了,我們都知道useEffect 就像componentDidMountcomponentDidUpdate的結合,你不能老是破壞這一條!”好吧,就算加了componentDidUpdate照樣無用:

class Child extends Component {
  state = {
    data: null
  };
  componentDidMount() {
    this.props.fetchData();
  }
  componentDidUpdate(prevProps) {
    // ?? This condition will never be true
    if (this.props.fetchData !== prevProps.fetchData) {
      this.props.fetchData();
    }
  }
  render() {
    // ...
  }
}

當然如此,fetchData是一個class方法?。ɑ蛘吣阋部梢哉f是class屬性 - 但這不能改變什么。)它不會因為狀態的改變而不同,所以this.props.fetchDataprevProps.fetchData始終相等,因此不會重新請求。那我們刪掉條件判斷怎么樣?

  componentDidUpdate(prevProps) {
    this.props.fetchData();
  }

等等,這樣會在每次渲染后都去請求。(添加一個加載動畫可能是一種有趣的發現這種情況的方式。)也許我們可以綁定一個特定的query?

render() {
    return <Child fetchData={this.fetchData.bind(this, this.state.query)} />;
  }

但這樣一來,this.props.fetchData !== prevProps.fetchData表達式永遠是true,即使query并未改變。這會導致我們總是去請求。

想要解決這個class組件中的難題,唯一現實可行的辦法是硬著頭皮把query本身傳入 Child 組件。Child 雖然實際并沒有直接使用這個query的值,但能在它改變的時候觸發一次重新請求:

class Parent extends Component {
  state = {
    query: 'react'
  };
  fetchData = () => {
    const url = 'https://hn.algolia.com/api/v1/search?query=' + this.state.query;
    // ... Fetch data and do something ...
  };
  render() {
    return <Child fetchData={this.fetchData} query={this.state.query} />;
  }
}

class Child extends Component {
  state = {
    data: null
  };
  componentDidMount() {
    this.props.fetchData();
  }
  componentDidUpdate(prevProps) {
    if (this.props.query !== prevProps.query) {
      this.props.fetchData();
    }
  }
  render() {
    // ...
  }
}

在使用React的class組件時,習慣于把不必要的props傳遞下去并且破壞父組件的封裝。

在class組件中,函數屬性本身并不是數據流的一部分。組件的方法中包含了可變的this變量導致我們不能確定無疑地認為它是不變的。因此,即使我們只需要一個函數,我們也必須把一堆數據傳遞下去僅僅是為了做“diff”。我們無法知道傳入的this.props.fetchData 是否依賴狀態,并且不知道它依賴的狀態是否改變了。

使用useCallback,函數完全可以參與到數據流中。我們可以說如果一個函數的輸入改變了,這個函數就改變了。如果沒有,函數也不會改變。感謝周到的useCallback,屬性比如props.fetchData的改變也會自動傳遞下去。

類似的,useMemo可以讓我們對復雜對象做類似的事情。

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是件挺笨拙的事。當我們需要將函數傳遞下去并且函數會在子組件的effect中被調用的時候,useCallback 是很好的技巧且非常有用?;蛘吣阆朐噲D減少對子組件的記憶負擔,也不妨一試。但總的來說Hooks本身能更好地避免傳遞回調函數。

10.說說競態

class Article extends Component {
  state = {
    article: null
  };
  componentDidMount() {
    this.fetchData(this.props.id);
  }
  componentDidUpdate(prevProps) {
    if (prevProps.id !== this.props.id) {
      this.fetchData(this.props.id);
    }
  }
  async fetchData(id) {
    const article = await API.fetchArticle(id);
    this.setState({ article });
  }
  // ...
}

有問題的原因是請求結果返回的順序不能保證一致。比如我先請求 {id: 10},然后更新到{id: 20},但{id: 20}的請求更先返回。請求更早但返回更晚的情況會錯誤地覆蓋狀態值。

這被叫做競態,這在混合了async / await(假設在等待結果返回)和自頂向下數據流的代碼中非常典型(propsstate可能會在async函數調用過程中發生改變)。

Effects并沒有神奇地解決這個問題,盡管它會警告你如果你直接傳了一個async 函數給effect。(我們會改善這個警告來更好地解釋你可能會遇到的這些問題。)

如果你使用的異步方式支持取消,那太棒了。你可以直接在清除函數中取消異步請求。

或者,最簡單的權宜之計是用一個布爾值來跟蹤它:

function Article({ id }) {
  const [article, setArticle] = useState(null);

  useEffect(() => {
    let didCancel = false;

    async function fetchData() {
      const article = await API.fetchArticle(id);
      if (!didCancel) {
        setArticle(article);
      }
    }

    fetchData();

    return () => {
      didCancel = true;
    };
  }, [id]);

  // ...
}

這篇文章討論了更多關于如何處理錯誤和加載狀態,以及抽離邏輯到自定義的Hook。我推薦你認真閱讀一下如果你想學習更多關于如何在Hooks里請求數據的內容。

十一.提高水準

在class組件生命周期的思維模型中,副作用的行為和渲染輸出是不同的。UI渲染是被props和state驅動的,并且能確保步調一致,但副作用并不是這樣。這是一類常見問題的來源。

而在useEffect的思維模型中,默認都是同步的。副作用變成了React數據流的一部分。對于每一個useEffect調用,一旦你處理正確,你的組件能夠更好地處理邊緣情況。

然而,用好useEffect的前期學習成本更高。這可能讓人氣惱。用同步的代碼去處理邊緣情況天然就比觸發一次不用和渲染結果步調一致的副作用更難。

這難免讓人擔憂如果useEffect是你現在使用最多的工具。不過,目前大抵還處理低水平使用階段。因為Hooks太新了所以大家都還在低水平地使用它,尤其是在一些教程示例中。但在實踐中,社區很可能即將開始高水平地使用Hooks,因為好的API會有更好的動量和沖勁。

我看到不同的應用在創造他們自己的Hooks,比如封裝了應用鑒權邏輯的useFetch或者使用theme context的useTheme 。你一旦有了包含這些的工具箱,你就不會那么頻繁地直接使用useEffect。但每一個基于它的Hook都能從它的適應能力中得到益處。

目前為止,useEffect主要用于數據請求。但是數據請求準確說并不是一個同步問題。因為我們的依賴經常是[]所以這一點尤其明顯。那我們究竟在同步什么?

長遠來看, Suspense用于數據請求 會允許第三方庫通過第一等的途徑告訴React暫停渲染直到某些異步事物(任何東西:代碼,數據,圖片)已經準備就緒。

當Suspense逐漸地覆蓋到更多的數據請求使用場景,我預料useEffect 會退居幕后作為一個強大的工具,用于同步props和state到某些副作用。不像數據請求,它可以很好地處理這些場景因為它就是為此而設計的。不過在那之前,自定義的Hooks比如這兒提到的是復用數據請求邏輯很好的方式。


閱讀原文

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容