iT邦幫忙

2021 iThome 鐵人賽

DAY 3
2
Modern Web

用30天更加認識 React.js 這個好朋友系列 第 3

Day3-React Hook 篇-認識 useEffect

  • 分享至 

  • xImage
  •  

今天要介紹的是常用的 hook,useEffect。

功用

React 元件本身是純函式,但還是有要處理到 side effect 的時候,而若要處理各種針對渲染出來的網頁後進行操作產生影響的 side effect,就設計出了 useEffect 去處理。

資料 fetch、事件監聽、setInterval、clearInterval、或手動改變 React component 中的 DOM 都是 side effect 的範例。

語法

useEffect(() => {
  // do ssomething...
}, [])

useEffect 第一個參數是一個 callback function,第二個參數是一個 dependency array,記錄 useEffect 內 callback function 用到的相關狀態、函式。

  1. 當第二個參數是空陣列時,只有第一次 render 會呼叫 callback 函式
  2. 沒有加第二個參數時,會在第一次 render 和每次元件 re-render 時呼叫
  3. 第二個參數陣列內有值時,會在第一次 render 和陣列內的元素值變更時呼叫

補充: React 底層機制是如何判斷陣列內的元素值是否有變更呢?

答案是透過 Object.is(),原始型別判斷值是否相同,物件型別判斷其參考是否相同。

如果物件型別使用 mutate 的方式更新,state 的 reference 相同就不會觸發重新渲染,這也是更新 state 要維持 Immutable 的原因之一。

不需加入至第二個參數陣列內的東西

  1. JS 內建函式/API(ex: fetch(), localStorage, setTimeout 回傳的物件)
  2. 巢狀物件,不需加入不相關的物件屬性,避免不需要的渲染
const { someProperty } = someObject;

// good
useEffect(() => {
  // 和 someProperty 相關的程式碼
}, [someObject.someProperty]);

// bad
useEffect(() => {
  // 和 someProperty 相關的程式碼
}, [someObject]);

清除 side effect

在 useEffect 中處理 side effect,有些需要做一些後續的處理,否則會有效能問題,有些則不用。

無需清除的 Effect

包含呼叫 api、DOM 操作等

清除方式就是回傳一個函式,是用來清理副作用的函式(cleanup),例如清除 timeout、取消一些訂閱、http 請求,去避免 memory leak 的問題。會在每次元件 下一次執行 effect 前 會被執行,用來清除前一次渲染所做的 side effect。

也就是說觸發 cleanup 的時機用步驟來說明的話,會如以下的樣子:

  1. 因為 state / props 改變後,React 元件進行 re-render
  2. 瀏覽器上我們會看到新的 UI
  3. 在新渲染元件的 useEffect 執行前,cleanup 前一次渲染的副作用
  4. React 運行此次渲染的 useEffect

Ref:
Synchronizing with Effects
A Complete Guide to useEffect

// 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

事件監聽(addEventListener、removeEventListener)、setTimeout、setInterval、觸發的動畫等...

範例1.

以下面的範例為例,若沒有加上 return () => clearInterval(interval); 就點擊 Unmount child component 會出現錯誤訊息

App.jsx

import { useState } from "react";
import Counter from "./Counter.js";

export default function App() {
  const [unmount, setUnmount] = useState(false);
  const renderDemo = () => !unmount && <Counter />;
  return (
    <div>
      <button onClick={() => setUnmount(true)}>Unmount child component</button>
      {renderDemo()}
    </div>
  );
}

Counter.jsx

import { useState, useEffect } from "react";

const Counter = () => {
  const [count, setCount] = useState(0);
  useEffect(() => {
    const interval = setInterval(function () {
      setCount((prev) => prev + 1);
    }, 1000);
    
    // 這裡必須做 clearInterval 的動作,否則會出現錯誤訊息
    return () => clearInterval(interval);
  }, []);
  return <p>and the counter counts {count}</p>;
};

export default Counter;

範例2.

將更新的樣式做復原

useEffect(() => {
  const node = ref.current;
  node.style.opacity = 1; // Trigger the animation
  return () => {
    node.style.opacity = 0; // Reset to the initial value
  };
}, []);

程式碼範例(codesandbox)

範例3. 中止 api request 避免 race condition

在 useEffect 內呼叫 api 時,也可以透過一個 Boolean flag 去控制,避免 api 發生 race conditions

export default function Page() {
  const [person, setPerson] = useState('Alice');
  const [bio, setBio] = useState(null);
  useEffect(() => {
    let ignore = false;
    setBio(null);
    fetchBio(person).then(result => {
      if (!ignore) {
        setBio(result);
      }
    });
    return () => {
      ignore = true;
    }
  }, [person]);

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

2023/01/18 補充: 也許你不一定要用到 useEffect

這段改寫自 React 官方文件-You Might Not Need an Effect 舉出幾個可以不用 useEffect 的情境,我自己看完也覺得很有幫助,所以整理成自己解讀的方式分享給讀者。

1. 可以不用在 props 更新時透過 useEffect 去重置 state

export default function ProfilePage({ userId }) {
  const [comment, setComment] = useState('');

  // 可以不用在 props 更新時去重置 state
  useEffect(() => {
    setComment('');
  }, [userId]);
  // ...
}

上面這段看的出來是例如有一個檔案頁面 ProfilePage,當切到別人的檔案頁面時,要去重置顯示的評論,但其實這類 id/key 值的東西,可以透過 key prop 屬性解決。

export default function ProfilePage({ userId }) {
  return (
    <Profile
      userId={userId}
      key={userId}
    />
  );
}

function Profile({ userId }) {
  // 根據 key 的特性去自動重置 state 的值
  const [comment, setComment] = useState('');
}

怕讀者不明白 key 在這裡的作用,所以這裡也附上 React 官方的文件和一個範例。

React key 的其他作用

參考文章: https://beta.reactjs.org/learn/sharing-state-between-components#controlled-and-uncontrolled-components

這裡透過給兩個 Counter 個別的 key,當 isPlayerA 去做切換時,會根據 key 值做判定是否不同 key,是的話會將其中一個 Counter 從 DOM 移除,再呈現另外一個,就可以去重置被移除元件的 state。

export default function Scoreboard() {
  const [isPlayerA, setIsPlayerA] = useState(true);

  return (
    <div>
      {isPlayerA ? (
        <Counter key="Taylor" person="Taylor" />
      ) : (
        <Counter key="Sarah" person="Sarah" />
      )}
      <button onClick={() => {
        setIsPlayerA(!isPlayerA);
      }}>
        Next player!
      </button>
    </div>
  );
}

function Counter({ person }) {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{person}'s score: {score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Add one
      </button>
    </div>
  );
}

2. 和事件處理函式共用邏輯

如以下範例,當事件觸發後更新 product,並透過 useEffect 重新呼叫去跳出提示訊息,這種情況也可以不用用 useEffect。而且這樣寫每次 product 被更新都會跳出提示,產生 bug。

function ProductPage({ product, addToCart }) {
  // 避免和事件處理相關的邏輯
  useEffect(() => {
    if (product.isInCart) {
      showNotification(`Added ${product.name} to the shopping cart!`);
    }
  }, [product]);

  function handleBuyClick() {
    addToCart(product);
  }

  function handleCheckoutClick() {
    addToCart(product);
    navigateTo('/checkout');
  }
}

改善的做法是將事件處理的邏輯封裝在函式,那的確根本就用不到 useEffect。

function ProductPage({ product, addToCart }) {
  function buyProduct() {
    addToCart(product);
    showNotification(`Added ${product.name} to the shopping cart!`);
  }

  function handleBuyClick() {
    buyProduct();
  }

  function handleCheckoutClick() {
    buyProduct();
    navigateTo('/checkout');
  }
}

3. 透過事件監聽呼叫 post 請求時,不應該在 useEffect 內呼叫 api

function Form() {
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');

  // 沒問題的範例,是在元件渲染時呼叫的 api
  useEffect(() => {
    post('/analytics/event', { eventName: 'visit_form' });
  }, []);

  // 不好的範例,因為是透過事件更新 state 去觸發 useEffect
  const [jsonToSubmit, setJsonToSubmit] = useState(null);
  useEffect(() => {
    if (jsonToSubmit !== null) {
      post('/api/register', jsonToSubmit);
    }
  }, [jsonToSubmit]);

  function handleSubmit(e) {
    e.preventDefault();
    setJsonToSubmit({ firstName, lastName });
  }
}

改良:

function Form() {
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');

  function handleSubmit(e) {
    e.preventDefault();
    // 直接在函式裡呼叫 api
    post('/api/register', { firstName, lastName });
  }
}

4. 訂閱外部的 store,使用 useSyncExternalStore 取代 useEffect


上一篇
Day2-React Hook 篇-認識 useState
下一篇
Day4-React Hook 篇-認識 useRef & useImperativeHandle
系列文
用30天更加認識 React.js 這個好朋友33
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言