iT邦幫忙

2023 iThome 鐵人賽

DAY 11
0
Modern Web

react 學習記錄系列 第 11

[Day11]我的 react 學習記錄 - useEffect

  • 分享至 

  • xImage
  •  

這篇文章的主要內容

簡單介紹 useEffect


useEffect

useEffect 是 react 裡面常常使用到到 hook 但是在使用上有很多需要注意的地方,希望可以在這一篇文章清楚的介紹 useEffect 的使用方法跟我知道的注意事項。


Syntax

useEffect(setup, dependencies?)

setup: 一個 function,當 react 呼叫 useEffect hook 的時候會執行這個 function,可以回傳一個 clean up function,當元件卸載或是 re-render 時,執行 useEffect 之前 會先執行 clean up function。

dependencies?: 一個 array,放著 setup function 裡面使用到外部變數,當元件 re-render 時會使用 Object.is() 一個一個做比較,如果其中一個是 false 的時候會執行 clean up function 然後再執行 setup function。

注意事項

  • hook 只能在元件裡使用,且只能在元件的最外層使用,不能放在迴圈或是判斷式裡,如果有需要可以建立一個新的子元件,放在子元件裡。
  • 如果沒有要透過 side effect 來同步狀態到元件外或同步狀態到元件內的話,也許你不需要使用 useEffect
  • 在 react strict mode 的情況下,在元件初始化的時候 react 會快速的 mount -> unmount -> mount 你的元件,如果出現了預期外的錯誤有可能你會需要一個 clean up function 來處理。

useEffect 執行時機

先用 console 來觀察一下 useEffect 的執行時機。

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

  useEffect(() => {
    console.log("useEffect");
    return () => {
      console.log("clean up");
    };
  });

  function addCount() {
    setCount(count + 1);
  }

  console.log("component render");
  return (
    <div>
      <h1>Count: {count}</h1>
      {console.log("in jsx")}
      <button onClick={addCount}>add name</button>
    </div>
  );
}

useEffect1

初次 render: component -> jsx -> useEffect
re-render: component -> jsx -> cleanup -> useEffect

可以注意到在初次 render 時 useEffect 是在 JSX render 完後才執行的,當 state 改變時也是在 JSX 都 render 完之後先執行 clean up 然後才執行 useEffect,看起來有點不太直覺。

useEffect 執行的時間是在 component 的 render 執行結束後才執行,因為 useEffect 的最常的用途就是用來同步元件的狀態或是取得外部的資源。


透過訂閱外部資源來把外部狀態同步到元件內

import { useEffect } from 'react';
import { createConnection } from './chat.js';

function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState('https://localhost:1234');

  useEffect(() => {
  	const connection = createConnection(serverUrl, roomId);
    connection.connect();
  	return () => {
      connection.disconnect();
  	};
  }, [serverUrl, roomId]);
  // ...
}

這邊使用 react 官方提供的範例來說明。

在這一個 useEffect 裡面有兩個參數,第一個就是執行訂閱的 function ,第二個就是在 useEffect 裡面有使用到的變數,這個範例裡面是 serverUrlroomId

元件 re-render 時如果 serverUrlroomId,不會改變,所以 useEffect 並不會再次執行。

先假設訂閱系統的 serverUrl 不會改變,會改變的只有 roomId

roomId001 變成 002 時 useEffect 會先執行 clean up function 來取消訂閱,此時執行的是 舊的元件內的 clean up function 並不是新的,所以會取消訂閱 001 的 ChatRoom 然後重新訂閱 002 的 ChatRoom,並且此時 clean up function 裡面取消訂閱的 ChatRoom 也變成了 002

  • useEffect 執行時會伴隨著 新的 propsstate
  • cleanup function 執行時會伴隨著舊的 propsstate

如果沒有 clean up function 裡面沒有執行 disconnect 來取消舊的訂閱的話,就會發生當我切換到 002 的 ChatRoom 時 001 的訂閱尚未被取消,所以 server 還是會持續的更新 001 的資料造成資源浪費,那當我又從 002 切換成 001 時,又會再重新訂閱 001 但是 002 又沒有被取消,反而訂閱了兩次 001,所以當在 useEffect 執行訂閱行為時務必要在 clean up function 裡做取消的動作。


計時器

相似的情形還有 setInterval 或是 window.addEventListener
舉個例子,如果我設下計時器,並且希望每 0.5 秒我的 count 加 1,可能會這樣寫。

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

  useEffect(() => {
    setInterval(() => {
      setCount(count + 1);
      console.log("effect"); // 觀察 effect 執行
    }, 500);
  }, [count]);

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

會發現數字出現了不正常的跳動,而且 console 不斷地跑出非常大量的 effect。

useEffect

這是因為我們沒有在 clean up function 裡面做取消訂閱的動作,所以每次 render 的時候 react 都會建立一個新的計時器,如下。

  • 第 1 次 render -> count 值 = 0,計時器 0.5 秒後執行 0 + 1
  • 第 2 次 render -> count 值 = 1,計時器 0.5 秒後執行 1 + 1
  • 第 3 次 render -> count 值 = 2,計時器 0.5 秒後執行 2 + 1

所以才會出現這個奇怪的狀況,在 cleanup function 加上取消訂閱就好了。

useEffect(() => {
    const timer = setInterval(() => {
      setCount(count + 1);
      console.log("effect");
    }, 500);

    return () => {
      timer && clearInterval(timer);
    };
  }, [count]);

useEffect3

務必要記得有訂閱事件時要在 cleanup function 裡面加上取消訂閱的動作。


water fall

另外如果在 useEffect 裡面進行 fetch 動作時會還有可能會有另外一個狀況,就是 water fall,簡單來說就是 api 回來的時間無法掌控。

這邊使用 react 官方提供的範例做說明。
假設有一個 select 選單,當用戶選擇時會取得當下那個人的 Bio 資料。

// app.tsx
import { useState, useEffect } from "react";
import { fetchBio } from "./api.js";

export default function Page() {
  const [person, setPerson] = useState<string>("Alice");
  const [bio, setBio] = useState<string | null>(null);

  useEffect(() => {
    setBio(null); // fetch 執行之前先設定成 null 以顯示 Loading...
    fetchBio(person).then((result) => {
      setBio(result);
    });
  }, [person]);

  function handleSelect({ target }: React.ChangeEvent<HTMLSelectElement>) {
    setPerson(target.value);
  }

  return (
    <div>
      <select value={person} onChange={handleSelect}>
        <option value="Alice">Alice</option>
        <option value="Bob">Bob</option>
        <option value="Taylor">Taylor</option>
      </select>
      <hr />
      <p>
        <i>{bio ?? "Loading..."}</i>
      </p>
    </div>
  );
}

假設當 fetch 的對象是 Bob 時會晚 3 秒才回應。

// api.tsx
export async function fetchBio(person: string): Promise<string> {
  const delay = person === "Bob" ? 3000 : 200;
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve("This is " + person + "’s bio.");
    }, delay);
  });
}

會發現當你切到 Bob 然後再切換到別人的時候資料會出錯。

useEffect4

會發生這個狀況的原因很單純,因為我們不知道 api 的 response 什麼時候才會回來,如果比較晚才發送的 response 比先發送的 response 還要快就回來的話就會出現,先執行了新的 setState 之後又把執行了舊的 setState。

react 官方提供了一個簡單的解決方法,就是在 useEffect 的 cleanup function 裡面做狀態的控管。

useEffect(() => {
    let ignore = false; // 狀態管理
    setBio(null);
    fetchBio(person).then((result) => {
      if (!ignore) {
        setBio(result);
      }
    });
    return () => {
      ignore = true;
    };
  }, [person]);

這樣就解決了!

useEffect5

上面有提到 useEffect 的 function 執行時會伴隨著新的 props 跟 state,cleanup function 執行時會伴隨著舊的 props 跟 state ,所以當從 Bob 切換成別人的時候會執行 Bob cleanup function,所以會把 Bob useEffect 裡面的 ignore = true; 即使 Bob 的 response 比較晚才回來也不會執行 setState 去改變當前的狀態。


永遠對 dependencies array 誠實

function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState('https://localhost:1234');
  
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => connection.disconnect();
  }, []);
}

以 react 官方的範例來看,明明在 useEffect 裡面有使用到 react 元件內的 serverUrlroomId,但是沒有放在 dependencies 裡面,這樣會導致當 serverUrlroomId 改變,需要重新執行訂閱,但是卻沒有執行。

如果 serverUrlroomId 是不會改變的值,那可以不要放在 react 的元件內做宣告或是 state 管理,可以直接宣告在元件外。

const serverUrl = "https://localhost:1234";
const roomId = "001";
function ChatRoom() {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => connection.disconnect();
  }, []);
}

這樣 serverUrlroomId 永遠不會改變,所以可以不用放在 dependencies 裡,但是當 dependencies 是空 array 時,代表你的 useEffect 只會執行一次,再之後 state 改變 re-render 時都不會再次執行。


Synchronizing with Effects - react document

下一篇簡單介紹 useRef。
如果內容有誤再麻煩大家指教,我會盡快修改。

這個系列的文章會同步更新在我個人的 Medium,歡迎大家來看看 👋👋👋
Medium


上一篇
[Day10]我的 react 學習記錄 - react 如何運作跟 key 是什麼
下一篇
[Day12]我的 react 學習記錄 - useRef
系列文
react 學習記錄30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言