iT邦幫忙

2022 iThome 鐵人賽

DAY 17
0
Modern Web

你 React 了嗎? 30 天解鎖 React 技能系列 第 17

[DAY 17] useEffect 處理副作用

  • 分享至 

  • xImage
  •  

[情境劇場]

解師傅:餐廳請了歌手來駐唱,讓生意越來越好了,卻引來了隔壁小吃店的不滿,在我們版上留了負評

小當家:人紅是非多…這也是沒辦法的事,金錢才是王道啊!!


cover

Side Effect 副作用

在介紹 useEffect 先來認識什麼是 Effect,Effect 指的是「Side Effect」,簡稱 Effect ,中文是副作用的意思。

我們最常聽到的副作用,也就是醫生開藥的藥單上面都會寫哪些藥,上面寫著作用跟副作用,作用是緩解鼻塞、流鼻水,可能會伴隨的副作用是會想睡覺

在 JavaScript 的也是這樣的存在,「在執行函式或行為時,會導致原有的狀態被改變或有附加功能」,就稱之為 Side Effect,例如:改變全域變數狀態、發送 HTTP Request、手動操作 DOM 元素 、改變系統狀態等等


認識 useEffect

useEffect 就是來處理 Side Effect 的 Hook,早期的 Class Component 是使用生命週期 (lifecycle)來管理組件函式,但這讓相同邏輯的函式,被迫拆開在不同的生命週期

class FriendStatus extends React.Component {
  constructor(props) {
    super(props);
    this.state = { isOnline: null };
    this.handleStatusChange = this.handleStatusChange.bind(this);
  }

  componentDidMount() {
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }
  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }
  handleStatusChange(status) {
    this.setState({
      isOnline: status.isOnline
    });
  }

  render() {
    if (this.state.isOnline === null) {
      return 'Loading...';
    }
    return this.state.isOnline ? 'Online' : 'Offline';
  }
}

因此 useEffect 的設計在將其保持在一起,易於管理,在程式碼上也較簡潔許多

function FriendStatus(props) {
  const [isOnline, setIsOnline] = useState(null);

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    
    return function cleanup() {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

useEffect 使用方法

  • 從 react 中載入 useEffect
  • useEffect 帶入一個函式,函式裡帶入要執行的 effect
  • 如有 dependencies,將 dependencies 帶入第二個參數陣列

1. 從 react 中載入 useEffect

import { useEffect } from 'react';

2. useEffect 帶入一個函式,函式裡帶入要執行的 effect

useEffect(() => {
  //...要執行的 effect
})

useEffect 會在參數中帶入一個函式,而這個函式會在「畫面渲染完成」後被呼叫,函式裡面放入畫面完成後需要執行的 effect


3. 如有 dependencies,將 dependencies 帶入第二個參數陣列

useEffect(() => {
  //...要執行的 effect
}, [dependencies])

useEffect 第二個參數為一個陣列,陣列裡會放入需要重新渲染 effect 的依賴,我們稱之為 dependencies,如 dependencies 有變動,才會重新執行 effect


看個範例

useEffect

codesandbox 程式碼範例

這是一個單純的計數器功能,一開始畫面渲染完畢後,會接著執行 useEffect,執行結束後,一直到變更了 count 的值,才會再執行一次 useEffect

import { useState, useEffect } from "react";

export default function App() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log("useEffect");
  }, [count]);

  return (
    <div>
      <h1>Count: {count}</h1>
      <button
        onClick={() => {
          setCount((state) => state + 1);
        }}
      >
        Add Count
      </button>
    </div>
  );
}

嘿!為什麼畫面一開始就渲染兩次 ?

為什麼畫面渲染完畢後,會跑了兩次 console.log?

你可以點擊 src/index.js 檔案,會看到裡面有 Strict Mode 嚴格模式

root.render(
  <StrictMode>
    <App />
  </StrictMode>
);

Strict Mode 在開發模式下,為了檢測到渲染期生命週期的預期之外的 Side Effect,故意調用函式兩次,來幫助我們發現 Side Effect

這些函式有

  • Class component constructorrender 和 shouldComponentUpdate 方法
  • Class component 的靜態 getDerivedStateFromProps 方法
  • Function component 的內容
  • 狀態更新函式(setState 的第一個參數)
  • 函數傳遞至 useStateuseMemo 或 useReducer

如果我們在 index.js 下了 console.log(”index.js”),會發現 index.js 只渲染了一次

useEffect

問題就很清楚了,Strict Mode 的開發模式下確實會渲染兩次

Strict Mode 只會在開發模式中執行,故不會調用在正式環境

useEffect 使用的四種方式

codesandbox 程式碼範例

1. 只執行在畫面渲染後

useEffect(() => {
  console.log("mounted");
}, [])

不依賴於任何 props 或 state 的值,不需要重新執行的函式或行為,可以直接傳遞一個空陣列
如果你有學過 Class Component,就會像是 lifecycle 中的 componentDidMount


2. 組件更新就執行

useEffect(() => {
	console.log("updated");
})

只要任何組件發生變動就會執行,會像是 class lifecycle 中的 componentDidUpdate


3. 組件 dependencies 有變更才執行

useEffect(() => {
  console.log("updated with dependencies");
}, [count])

dependencies 發生改變才執行,如範例中的 count 如有變更,才會觸發 console.log,避免在每次 render 都進行昂貴的計算


4. 清理需被銷毀的函式

清理函式會在組件每次重新渲染時執行,先清除上次留下來的 effect,useEffect 會做的 4 個步驟:

  1. 判斷第二個參數的陣列是否一樣,如果一樣才會繼續
  2. 執行上一次存下來的清理函式
  3. 執行useEffect的內容
  4. 把 清理函式 存下來,供下次使用
useEffect(() => {
  const onMousedown = () => {
    console.log("mousedown");
  };
  window.addEventListener("mousedown", onMousedown);

	const timer = setInterval(() => {
    console.log('Hello React');
  }, 1000);

  return () => {
    console.log("cleanup");
    window.removeEventListener("mousedown", onMousedown);
		clearInterval(timer);
  };
});

在 useEffect 函式裡 return 一個函式,這個函式就是清除 effect 的函式,就像是 class lifecycle 中的 componentWillUnmount ,如果沒有將 effect 清除,當組件重新渲染,都會執行一個新的事件監聽,這很可能會發生錯誤,也會讓效能下降,常見的需要清除的函式如:setInterval、setTimeout、addEventListener…等


結語

認識了 useEffect 跟使用方法,讓我們得以處理 side effect,useEffect 跟之前的 class lifecycle 相比好寫很多,把相同邏輯放在一起也舒服很多~一起練習看看吧!/images/emoticon/emoticon12.gif


Reference

官方 Effect Hook


本文將同步更新至我的部落格
Lala 的前端大補帖



上一篇
[DAY 16] 好用的 React Developer Tools 偵錯工具
下一篇
[DAY 18] useMemo 緩存記憶體,避免重新渲染
系列文
你 React 了嗎? 30 天解鎖 React 技能30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
APPX Jim
iT邦新手 5 級 ‧ 2022-10-03 12:53:28

請問,不知有沒有方式 能夠寫程式偵測 是否有些遺留未清除的資源(變數/物件…)?

我要留言

立即登入留言