iT邦幫忙

2023 iThome 鐵人賽

DAY 6
0
Modern Web

React 走出新手村 系列 第 6

React 走出新手村-深入useEffect

  • 分享至 

  • xImage
  •  

複習useEffect

今天要來聊聊新手時期最痛恨的hook function — useEffect
https://ithelp.ithome.com.tw/upload/images/20230901/20129020Y6u31qYTDf.jpg

useEffect的誕生

首先,讓我們先來了解一下 useEffect 的誕生是為了處理怎樣的問題吧!有沒有這樣的情況,比如在前一篇那裡曾經介紹過的 search 功能,當我們設定的 state 經過更動後,想要讓他自動地跑一些其他連帶的 function。又或者更常見的 api 串接工作,幾乎要跟 useEffect 綁在一起了,在 React-query & SWR 還不普及的原生年代,幾乎大部分的教學都告訴你要用 useEffect 去打 api,但其中的原理大多要追朔到 class component 的作法,也就是整個 React 演化的歷史。

https://ithelp.ithome.com.tw/upload/images/20230901/20129020kqL3lydCwf.png

在早期 class component 的環境中都是以物件形式來區分 render 時機點,和前一篇模擬的範例很相似,也就是說所有渲染時機都有對應的位置和 function 可以去做更動,但缺點就是很攏長,那麼為了改善這個問題,於是 functional component 的出現解決了許多不便,但其連帶的 hooks function 設計理念也是從 class component 的使用情境下思考,並不像 solidjs 那樣重新設計避開 side effect 的使用問題,這也導致 useEffect 並不像其他 hooks 那樣單純,因為他本質上就是用來處理 side effect 的 hook。

https://ithelp.ithome.com.tw/upload/images/20230901/20129020TiLpybgyTW.png

從圖面可知,它是一個橫跨生命週期中三個渲染時機的hook function,所以自然比其他的hook function來得複雜,讓我們看看他的範例吧!

// 實際在component使用
useEffect(() => {
  // 下面 function 對應的是 componentDidMount() 。
  const timer = window.setInterval(() => {
    setCount((pre) => pre + 1)
  }, 1000)
  // clean up function 對應的原本的 componentWillUnmount() 的地方
  return () => window.clearInterval(timer)
  // 第二個參數為 dependencies array,在陣列裡面的參數會使得 useEffect 去判斷該欄位的值是否需要更新,
  // 如果需要則會依據該參數是否更新而觸發第一個帶入的 update function 而執行重新渲染。
}, [])

那麼,以上的基本概念是怎麼搞混的呢?讓我們試者加入常用情境 - 串接 api:

// 實際在component使用
useEffect(() => {
  // 這裡是避免 react.18 二次渲染的解法,很多朋友都是直接處理 fetch(),
  // 也就是沒有處理 clean up 的部分,那這裡就是處理fetch clean function 的做法
  const controller = new AbortController();
  const signal = controller.signal;
  fetch(API_URL, {
      signal: signal
  })
  .then((response) => response.json())
  .then((response) => {
    // 成功之後的處理
  });
  // 清除 request componentWillUnmounts
  return () => controller.abort();
}, []);

看起來都能夠正常運行對吧!但如果我會因為 state change 而重新判斷他需不需要去重新取資料呢?如下:

// 實際在component使用
// const [params, setParams] = useState("")
// 上面的話不會有問題,但下面的作法就會有問題了
const [params, setParams] = useState({count: "", limit: ""})

useEffect(() => {
  // 這裡是避免 react.18 二次渲染的解法,很多朋友都是直接處理 fetch(),
  // 也就是沒有處理 clean up 的部分,那這裡就是處理fetch clean function 的做法
  const controller = new AbortController();
  const signal = controller.signal;
  fetch(API_URL + `?count=${params.count}&limit${params.limit}`, {
      signal: signal
  })
  .then((response) => response.json())
  .then((response) => {
    // 成功之後的處理
  });
  return () => controller.abort();
}, [params]);

// 這個模擬params change
// const onParamsChange = (e) => setParams(e.target.value); // 對應params為string
const onParamsCountChange = (e) => setParams((pre) => {...pre, count: e.target.value});
const onParamsLimitChange = (e) => setParams((pre) => {...pre, limit: e.target.value});

如果 params 只單純是字串的話,功能還不會有問題,但是如果是物件或是陣列格式的話就必須要注意了,因為在 dependencies array 裡面是採 shallow compare 的機制,簡單來說就是採 js 的比較機制,也就是新手常常搞混的地方,下面為簡單解釋:

5 === 5; // true
5 === 10; // false

true === true; // true
true === false; // false

"hello" === "hello"; // true
const hello = "hello"; // 更換指向
hello === "hello" // true

({a: "a"}) === { a: "a" }; // false, 
// 因為 js 在物件型別的處理上是採 by reference,
// 所以即便 key & value 都相等也不等於相同物件
const obj = { a: "a" };
obj === obj; // true
obj === { a: "a" }; // false

[] === []; // false
// 相同的情況也發生在陣列上
[5] === [5]; // false
// 那react怎麼判斷dependencies array內部的否相等呢,如下:
const deepEqual = (old, new) => old.length === new.length && old.every((el, i) => el === new[i]) 

deepEqual([], []); // true
deepEqual([5], []); // false
deepEqual([5], [5]); //true
deepEqual([true], ["true"]); // false
deepEqual([hello], [hello]); // true
deepEqual([obj], [obj]); // true

const copyObj = obj;
deepEqual([obj], [copyObj]); // true
deepEqual([obj], [{ a: "a" }]); // false,因為 reference 是不同的
// 這就是很多使用上的錯誤盲區,它會判斷裡面的內容,但基本上仍然是參照 js 的判斷機制
// 所以在 dependencies array 內部,必須盡量避開 by reference 的判斷情境
// 如果是物件看能不能往下取一層就好,
// 如果必要用到物件或陣列整個來判斷你的 updating function 是否需要執行的話,
// 考慮採用 useMemo 或 useCallback 額外做處理。

那麼讓我們再回頭修正一下之前的問題吧!

// 實際在component使用
const [params, setParams] = useState({count: "", limit: ""})

useEffect(() => {
  const controller = new AbortController();
  const signal = controller.signal;
  if(API_URL) { // 這裡我加了判斷,避免參數沒有接到fetch會失敗
    fetch(API_URL + `?count=${params.count}&limit=${params.limit}`, {
        signal: signal
    })
    .then((response) => response.json())
    .then((response) => {
      // 成功之後的處理
    });
    return () => controller.abort();
  }
}, [API_URL, params.count, params.limit]); 
// 這裡就不能只擺 params 會有剛才提到的問題
// 必須往下取到 by value 的值

// 這個模擬params change
const onParamsCountChange = (e) => setParams((pre) => {...pre, count: e.target.value});
const onParamsLimitChange = (e) => setParams((pre) => {...pre, limit: e.target.value});

總結

在以上的講解之後,希望大家在使用的同時記住以下幾點:

  1. 不要忘記你的 dependencies array 的存在。
  2. 要注意你放進 dependencies array 中的參數型別,如果是 by reference 的資料(ex: object, array…), 要記得額外用 useMemo 或 useCallback 來處理。
  3. 如果不想看到 React 18 渲染兩次的話,記得要處理 clean up function。

那麼今天的分享就到這裡,下一篇我們來聊聊useRef吧!

給全新手的大禮包

React基本Hook教學

參考資料

useFootgun meme
生命週期圖片1
生命週期圖片2
Jack Herrington Medium


上一篇
React 走出新手村-深入useState
下一篇
React 走出新手村-深入useRef
系列文
React 走出新手村 31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言