用了這麼長時間的 React useEffect hook,你用對了嗎?

useEffect 是 React Hooks 的核心,要保證理解它的運行機制和正確的使用方法才能避免這樣那樣的坑。在之前的工做中,由於它我碰到過無數個坑,好比拿到的值是舊的,該執行的時候不執行,不應執行的時候執行了……javascript

因此爲了不如上尷尬、節省絞盡腦汁找 bug 的時間、保護我們的髮際線,必定要認真學會如何正確使用 useEffect hook。java

什麼是 Effect

俗話說,知已知彼,百戰不殆,咱們先了解下神馬是 Effect。其實你們在開發過程當中或多或少的都接觸過 side effect 的概念,即ajax

反作用
—— 對於數據抓取,註冊監聽事件,修改 DOM 元素等馬後炮式的操做都屬於反作用,由於咱們渲染出來的頁面是靜態的,任何在以後的操做都會對它產生影響,因此才稱之爲反作用。而 useEffect 則是專門用來編寫
反作用
代碼的,這也是 React 的核心所在。

與生命週期的關係

目前市面上的文章,包括官方文檔都讓咱們把 useEffect 想象成 componentDidMountcomponentDidUpdatecomponentWillUnmount 三個生命週期的結合體。其實並否則,若是非要把 useEffect 的運行機制往生命週期上靠,會形成一些邏輯上的困惑,進而產生 bug。咱們所要作的,就是把 useEffect 當成一個全新的特性,專門爲函數式組件服務的,這樣用起來纔不會迷茫。下面咱們經過實例來演示它的各類用法。json

運行時機

useEffect 必然會在 render 的時候執行一次,其餘的運行時機取決於如下狀況:數組

  • 有沒有第二個參數。useEffect hook 接受兩個參數,第一個是要執行的代碼,第二個是一個數組,指定一組依賴的變量,其中任何一個變量發生變化時,此 effect 都會從新執行一次。
  • 有沒有返回值。 useEffect 的執行代碼中能夠返回一個函數,在每一次新的 render 進行前或者組件 unmount 之時,都會執行此函數,進行清理工做。

咱們先看一個簡單的例子,想看完整代碼和隨意把玩的,請點擊下邊按鈕瀏覽器

Edit without the second parameter

咱們首先看最頂層 <App /> 的代碼:app

function App() {
  const [showList, setShowList] = useState(false);
  const [postCount, setPostCount] = useState(5);

  return (
    <div className="App"> <button onClick={() => setShowList(!showList)}> {showList ? "隱藏" : "顯示"} </button> <button onClick={() => setPostCount(previousCount => previousCount + 1)}> 增長數量 </button> {showList && <PostList count={postCount} />} </div> ); } 複製代碼

此組件用來顯示一系列的文章列表,以及控制文章列表是否顯示的按鈕和控制顯示多少條文章的按鈕。咱們用 showList state 來控制 <PostList /> 的顯示與否。這是爲了讓 <PostList /> 組件 unmount 再 render ,以證實每次它 render 和 unmount 的時候,useEffect hook 都會跑一次。 <PostList /> 組件的代碼以下:異步

function PostList({ count = 5 }) {
  useEffect(() => {
    let p = document.createElement("p");
    p.innerHTML = `當前文章數量:${count}`;
    document.body.append(p);
  });

  return (
    <ul> {new Array(count).fill("文章標題").map((value, index) => { return ( <li key={index}> {value} {index + 1} </li> ); })} </ul>
  );
}
複製代碼

該組件展現了一個 <ul> 列表,爲了簡單起見,生成了一些無聊的文章標題。咱們重點來看一下 useEffect 所作的操做:async

  • 建立一個 p 元素
  • 設置 p 的文本爲當前文章的數量
  • 追加 pbody 的最後

在這裏,此 effect 並無返回任何值,也沒有給它傳遞任何一個參數,那會是什麼樣的效果呢?
沒返回值ide

答案是,此 effect 會在每次 countshowList 改變時每點擊一次 顯示增長數量 按鈕,咱們新追加的 p 都會再追加一次。這也是形成內容泄露的坑,若是咱們在這裏添加了太多耗內存的東西而沒有清理,不用多久瀏覽器就崩潰了~ 解決方法很簡單,給 useEffect 添加一個返回值,並在裏邊刪除咱們追加的 p 元素便可:

useEffect(() => {
  let p = document.createElement("p");
  p.innerHTML = `當前文章數量:${count}`;
  document.body.append(p);

  return () => {
    p.remove();
  };
});
複製代碼

這樣咱們在點擊按鈕的時候,確保只有一個 p 在當前頁面上。看,這樣寫起來是否是比分散在 componentDidMountcomponentWillUnmount 中方便多了?咱們能夠方便的在同一個做用域中方便的拿到 p 的引用,直接刪除它便可。

有返回值

類實際工做的例子 - 抓取數據

爲了繼續深刻 useEffect hook,我仿照實際工做遇到的狀況,編寫了一個例子,這裏咱們用 useEffect 進行數據抓取,一樣的顯示博客文章列表,完整代碼請點擊下方按鈕查看:

Edit async loading

在本例中,<PostList />組件的代碼作了一些修改,首先咱們定義兩個新的 state:

  • posts。保存遠程加載的文章列表
  • loading。記錄 ajax 請求狀態
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(false);
複製代碼

咱們一開始以爲 fetch 是異步操做,那麼得給 useEffect hook 傳遞個 async 的函數吧?錯,傳遞給 useEffect 的函數不能是 async 的, 由於 async 的本質是返回一個 Promise,而 useEffect 惟一接收的返回值是個函數。使用 async 會收到如下異常:

Warning: An effect function must not return anything besides a function, which is used for clean-up.

// 錯誤
useEffect(async () => {
  // const response = await fetch("https://jsonplaceholder.typicode.com/posts");
});
複製代碼

正確的寫法是,把抓取數據的邏輯定義到一個單獨的函數中,而後在 useEffect 中調用它:

useEffect(() => {
  // const response = await fetch("https://jsonplaceholder.typicode.com/posts");

  const loadPosts = async () => {
    setLoading(true);
    const response = await fetch(
      `https://jsonplaceholder.typicode.com/posts?_limit=${count}`
    );
    const data = await response.json();
    setPosts(data);
    setLoading(false);
  };

  loadPosts();
}, [count]);
複製代碼

它所作的操做是,在請求數據前,把 loading 狀態設置爲 true,而後根據 count 的值去取對應數量的文章列表,把返回值更新到 posts state 中,再把 loading 設置爲 false。最後根據 loading 的狀態,咱們顯示 加載中文章列表

if (loading) return <div>loading...</div>;

return (
  <ul> {posts.slice(0, count).map((post, index) => { return <li key={post.id}>{post.title}</li>; })} </ul>
);
複製代碼

加載數據

第二個參數

上邊的例子中咱們給 useEffect 傳遞了第二個參數,並把 count 做爲依賴的值,每當 count 變化時,此 effect 都會從新執行一次,去加載新的數據。另外,若是咱們隱藏列表,再點擊 顯示 按鈕時,effect 也會再跑一次,由於點擊隱藏時,<PostList /> 組件被 unmount ,而後再次顯示時會從新 render ,咱們能夠根據 loading... 這個標誌就能夠看出來了。

若是咱們去掉第二個參數,那麼就會陷入死循環的坑,爲何呢?由於 effect 執行時,會更新 postsloading 這兩個 state,而 state 變化時,組件又會從新 render 一次,根據 useEffect 在每次 render 必執行一次的定律不可貴出結論。

那麼若是咱們給它一個空數組呢?那就不管怎麼點擊增長數量,此 effect 都不會從新執行,致使永遠只加載默認 5 篇文章。

加載數據-無依賴數組

添加其餘屬性

咱們能夠再試試添加一個其餘屬性來測試 useEffect 依賴數組的特性。在 <App /> 組件中咱們添加一個佈局狀態 vertical 和修改佈局的按鈕,用來控制 <PostList> 組件的橫向、縱向佈局:

// APP
function App() {
  // 其它代碼省略
  const [vertical, setVertical] = useState(true);

  return (
    <div className="App"> {/* 其它代碼省略 */} <button onClick={() => setVertical(prev => !prev)}>更改佈局</button> {showList && <PostList count={postCount} vertical={vertical} />} </div> ); } // PostList function PostList({ count = 5, vertical = false }) { // 其它代碼省略 useEffect(() => { // const response = await fetch("https://jsonplaceholder.typicode.com/posts"); const loadPosts = async () => { setLoading(true); const response = await fetch( `https://jsonplaceholder.typicode.com/posts?_limit=${count}` ); const data = await response.json(); setPosts(data); setLoading(false); }; loadPosts(); }, [count, vertical]); // 在這裏添加 vertical 做爲依賴 // 其它代碼省略 } 複製代碼

在這裏咱們給第二個參數添加了 vertical 依賴,這樣每次點擊 更改佈局 按鈕時,文章列表都會加載一次,這種適合在佈局改變時須要從新請求數據的狀況:
加載數據-依賴layout

若是不須要從新加載數據,只須要把 vertical 從依賴數組裏去掉就能夠了。

加載數據-不依賴layout

劃重點

看看你們對頻繁使用的 useEffect 的用法用對了沒有?來標一下重點:

  1. 它可不徹底是 componentDidMountcomponentDidUpdatecomponentWillUnmount 三個生命週期的結合體哦(人家有本身的想法)。
  2. 會在每次 render 的時候一定執行一次。
  3. 若是返回了函數,那麼在下一次 render 以前或組件 unmount 以前一定會運行一次返回函數的代碼。
  4. 若是指定了依賴數組,且不爲空,則當數組裏的每一個元素髮生變化時,都會從新運行一次。
  5. 若是數組爲空,則只在第一次 render 時執行一次,若是有返回值,則同 3。
  6. 若是在 useEffect 中更新了 state,且沒有指定依賴數組,或 state 存在於依賴數組中,就會形成死循環。

你們掌握了嗎?有什麼問題歡迎評論或私信我!若是以爲文章有幫助請關注博主我哦,感謝,比心。

相關文章
相關標籤/搜索