React 允許你在 JSX 中新增事件處理器。事件處理器是你自行定義的函式,會在點擊、滑鼠懸停、表單輸入框獲得焦點等互動時觸發。

你將學到

  • 編寫事件處理器的不同方式
  • 如何從父元件傳遞事件處理邏輯
  • 事件如何傳播以及如何停止它們

新增事件處理器

要新增事件處理器,你首先要定義一個函式,然後將它作為 prop 傳遞給適當的 JSX 標籤。例如,這裡有一個目前還沒有任何功能的按鈕

export default function Button() {
  return (
    <button>
      I don't do anything
    </button>
  );
}

你可以按照以下三個步驟,讓它在使用者點擊時顯示訊息

  1. 在你的 Button 元件內部宣告一個名為 handleClick 的函式。
  2. 在該函式內實作邏輯(使用 alert 顯示訊息)。
  3. onClick={handleClick} 新增到 <button> JSX 中。
export default function Button() {
  function handleClick() {
    alert('You clicked me!');
  }

  return (
    <button onClick={handleClick}>
      Click me
    </button>
  );
}

你定義了 handleClick 函式,然後將它作為 prop 傳遞<button>handleClick 是一個事件處理器。 事件處理器函式

  • 通常定義在你的元件內部
  • 名稱以 handle 開頭,後跟事件的名稱。

按照慣例,事件處理器的名稱通常是 handle 後跟事件名稱。你會經常看到 onClick={handleClick}onMouseEnter={handleMouseEnter} 等等。

或者,你可以在 JSX 中內聯定義事件處理器

<button onClick={function handleClick() {
alert('You clicked me!');
}}>

或者,更簡潔地使用箭頭函式

<button onClick={() => {
alert('You clicked me!');
}}>

所有這些樣式都是等效的。內聯事件處理器適用於簡短的函式。

陷阱

傳遞給事件處理器的函式必須是被傳遞,而不是被呼叫。例如

傳遞一個函式(正確)呼叫一個函式(錯誤)
<button onClick={handleClick}><button onClick={handleClick()}>

差異很微妙。在第一個例子中,handleClick 函式作為 onClick 事件處理器被傳遞。這告訴 React 記住它,並且只在使用者點擊按鈕時才呼叫你的函式。

在第二個例子中,handleClick() 末尾的 () 會在渲染期間_立即_觸發函式,而無需任何點擊。這是因為 JSX {} 中的 JavaScript 會立即執行。

當你編寫內聯程式碼時,相同的陷阱會以不同的方式出現

傳遞一個函式(正確)呼叫一個函式(錯誤)
<button onClick={() => alert('...')}><button onClick={alert('...')}>

像這樣傳遞內聯程式碼不會在點擊時觸發,它會在每次元件渲染時觸發

// This alert fires when the component renders, not when clicked!
<button onClick={alert('You clicked me!')}>

如果你想內聯定義你的事件處理器,請將它包裝在一個匿名函式中,如下所示

<button onClick={() => alert('You clicked me!')}>

這將建立一個稍後呼叫的函式,而不是每次渲染都執行裡面的程式碼。

在這兩種情況下,你想傳遞的都是一個函式

  • <button onClick={handleClick}> 傳遞 handleClick 函式。
  • <button onClick={() => alert('...')}> 傳遞 () => alert('...') 函式。

閱讀更多關於箭頭函式的資訊。

在事件處理器中讀取 props

由於事件處理器是在元件內部宣告的,因此它們可以存取元件的 props。這裡有一個按鈕,當點擊它時,會顯示一個包含其 message prop 的提示訊息。

function AlertButton({ message, children }) {
  return (
    <button onClick={() => alert(message)}>
      {children}
    </button>
  );
}

export default function Toolbar() {
  return (
    <div>
      <AlertButton message="Playing!">
        Play Movie
      </AlertButton>
      <AlertButton message="Uploading!">
        Upload Image
      </AlertButton>
    </div>
  );
}

這讓這兩個按鈕顯示不同的訊息。嘗試更改傳遞給它們的訊息。

將事件處理器作為 props 傳遞

通常,您會希望父元件指定子元件的事件處理器。考慮按鈕:根據您使用 Button 元件的位置,您可能想要執行不同的函式 — 也許一個播放電影,另一個上傳圖片。

要做到這一點,請將元件從其父元件接收的 prop 作為事件處理器傳遞,如下所示:

function Button({ onClick, children }) {
  return (
    <button onClick={onClick}>
      {children}
    </button>
  );
}

function PlayButton({ movieName }) {
  function handlePlayClick() {
    alert(`Playing ${movieName}!`);
  }

  return (
    <Button onClick={handlePlayClick}>
      Play "{movieName}"
    </Button>
  );
}

function UploadButton() {
  return (
    <Button onClick={() => alert('Uploading!')}>
      Upload Image
    </Button>
  );
}

export default function Toolbar() {
  return (
    <div>
      <PlayButton movieName="Kiki's Delivery Service" />
      <UploadButton />
    </div>
  );
}

在這裡,Toolbar 元件渲染一個 PlayButton 和一個 UploadButton

  • PlayButtonhandlePlayClick 作為 onClick prop 傳遞給內部的 Button
  • UploadButton() => alert('正在上傳!') 作為 onClick prop 傳遞給內部的 Button

最後,您的 Button 元件接受一個名為 onClick 的 prop。它使用 onClick={onClick} 將該 prop 直接傳遞給內建的瀏覽器 <button>。這告訴 React 在點擊時呼叫傳遞的函式。

如果您使用設計系統,像按鈕這樣的元件通常包含樣式但不指定行為。相反,像 PlayButtonUploadButton 這樣的元件會將事件處理器向下傳遞。

命名事件處理器 props

<button><div> 這樣的內建元件僅支援瀏覽器事件名稱,例如 onClick。但是,當您構建自己的元件時,您可以根據自己的喜好命名事件處理器 props。

按照慣例,事件處理器 props 應以 on 開頭,後跟一個大寫字母。

例如,Button 元件的 onClick prop 可以稱為 onSmash

function Button({ onSmash, children }) {
  return (
    <button onClick={onSmash}>
      {children}
    </button>
  );
}

export default function App() {
  return (
    <div>
      <Button onSmash={() => alert('Playing!')}>
        Play Movie
      </Button>
      <Button onSmash={() => alert('Uploading!')}>
        Upload Image
      </Button>
    </div>
  );
}

在此範例中,<button onClick={onSmash}> 顯示瀏覽器 <button>(小寫)仍然需要一個名為 onClick 的 prop,但是您的自定義 Button 元件接收的 prop 名稱由您決定!

當您的元件支援多種互動時,您可以根據應用程式特定的概念來命名事件處理器 props。例如,這個 Toolbar 元件接收 onPlayMovieonUploadImage 事件處理器。

export default function App() {
  return (
    <Toolbar
      onPlayMovie={() => alert('Playing!')}
      onUploadImage={() => alert('Uploading!')}
    />
  );
}

function Toolbar({ onPlayMovie, onUploadImage }) {
  return (
    <div>
      <Button onClick={onPlayMovie}>
        Play Movie
      </Button>
      <Button onClick={onUploadImage}>
        Upload Image
      </Button>
    </div>
  );
}

function Button({ onClick, children }) {
  return (
    <button onClick={onClick}>
      {children}
    </button>
  );
}

請注意,App 元件不需要知道 Toolbar 將如何處理 onPlayMovieonUploadImage。這是 Toolbar 的實作細節。在這裡,Toolbar 將它們作為 onClick 處理器向下傳遞給其 Button,但它之後也可以透過鍵盤快捷鍵觸發它們。根據應用程式特定的互動(例如 onPlayMovie)命名 props,讓您日後可以彈性地更改它們的使用方式。

注意事項

請確定您為事件處理器使用了適當的 HTML 標籤。例如,要處理點擊事件,請使用 <button onClick={handleClick}> 而不是 <div onClick={handleClick}>。使用真正的瀏覽器 <button> 可以啟用內建的瀏覽器行為,例如鍵盤導航。如果您不喜歡按鈕的預設瀏覽器樣式,並且希望它看起來更像連結或其他 UI 元素,您可以使用 CSS 來實現。 深入瞭解如何撰寫可存取的標記。

事件傳播

事件處理器也會攔截來自元件任何子元件的事件。我們說一個事件會「向上冒泡」或「傳播」:它從事件發生的位置開始,然後向上傳播到樹狀結構。

這個 <div> 包含兩個按鈕。<div> *和* 每個按鈕都有自己的 onClick 處理器。您認為當您點擊一個按鈕時,哪些處理器會觸發?

export default function Toolbar() {
  return (
    <div className="Toolbar" onClick={() => {
      alert('You clicked on the toolbar!');
    }}>
      <button onClick={() => alert('Playing!')}>
        Play Movie
      </button>
      <button onClick={() => alert('Uploading!')}>
        Upload Image
      </button>
    </div>
  );
}

如果您點擊任一按鈕,它的 onClick 會先執行,接著是父層 <div>onClick。因此會出現兩則訊息。如果您點擊工具列本身,則只有父層 <div>onClick 會執行。

陷阱

在 React 中,所有事件都會向上傳播,除了 onScroll 事件,它只會在您附加它的 JSX 標籤上觸發。

停止事件傳播

事件處理器會接收一個 **事件物件** 作為其唯一參數。按照慣例,它通常被稱為 e,代表「事件」。您可以使用此物件來讀取有關事件的資訊。

該事件物件也允許您停止事件傳播。如果您要防止事件到達父元件,則需要呼叫 e.stopPropagation(),就像這個 Button 元件所做的一樣。

function Button({ onClick, children }) {
  return (
    <button onClick={e => {
      e.stopPropagation();
      onClick();
    }}>
      {children}
    </button>
  );
}

export default function Toolbar() {
  return (
    <div className="Toolbar" onClick={() => {
      alert('You clicked on the toolbar!');
    }}>
      <Button onClick={() => alert('Playing!')}>
        Play Movie
      </Button>
      <Button onClick={() => alert('Uploading!')}>
        Upload Image
      </Button>
    </div>
  );
}

當您點擊按鈕時

  1. React 會呼叫傳遞給 <button>onClick 處理器。
  2. Button 中定義的該處理器會執行以下操作:
    • 呼叫 e.stopPropagation(),防止事件進一步向上冒泡。
    • 呼叫 onClick 函式,它是從 Toolbar 元件傳遞的屬性。
  3. Toolbar 元件中定義的該函式會顯示按鈕自身的警示訊息。
  4. 由於事件傳播已停止,父層 <div>onClick 處理器*不會* 執行。

由於 e.stopPropagation() 的作用,點擊按鈕現在只會顯示一則警示訊息(來自 <button>),而不是兩則(來自 <button> 和父工具列 <div>)。點擊按鈕與點擊周圍的工具列不同,因此停止傳播對於此 UI 來說是有意義的。

深入探討

捕獲階段事件

在極少數情況下,您可能需要捕獲子元素上的所有事件,*即使它們已停止傳播*。例如,也許您想將每次點擊記錄到分析中,無論傳播邏輯如何。您可以透過在事件名稱後面加上 Capture 來做到這一點。

<div onClickCapture={() => { /* this runs first */ }}>
<button onClick={e => e.stopPropagation()} />
<button onClick={e => e.stopPropagation()} />
</div>

每個事件都分三個階段傳播:

  1. 它向下傳播,呼叫所有 onClickCapture 處理器。
  2. 它執行被點擊元素的 onClick 處理器。
  3. 它向上傳播,呼叫所有 onClick 處理器。

捕獲事件適用於路由器或分析等程式碼,但您可能不會在應用程式程式碼中使用它們。

傳遞處理器作為傳播的替代方案

請注意,此點擊處理器如何執行一行程式碼,*然後* 呼叫由父層傳遞的 onClick 屬性。

function Button({ onClick, children }) {
return (
<button onClick={e => {
e.stopPropagation();
onClick();
}}>
{children}
</button>
);
}

您也可以在呼叫父層 onClick 事件處理器之前,在此處理器中新增更多程式碼。此模式提供了一種傳播的*替代方案*。它允許子元件處理事件,同時也允許父元件指定一些額外行為。與傳播不同,它不是自動的。但這種模式的好處是,您可以清楚地追蹤因某些事件而執行的整個程式碼鏈。

如果您依賴傳播,並且難以追蹤哪些處理器執行以及原因,請改用這種方法。

防止預設行為

某些瀏覽器事件具有與之關聯的預設行為。例如,當點擊 <form> 內部的按鈕時發生的提交事件,預設情況下會重新載入整個頁面。

export default function Signup() {
  return (
    <form onSubmit={() => alert('Submitting!')}>
      <input />
      <button>Send</button>
    </form>
  );
}

您可以呼叫事件物件上的 e.preventDefault() 來阻止這種情況發生。

export default function Signup() {
  return (
    <form onSubmit={e => {
      e.preventDefault();
      alert('Submitting!');
    }}>
      <input />
      <button>Send</button>
    </form>
  );
}

不要混淆 e.stopPropagation()e.preventDefault()。它們都很有用,但彼此無關。

事件處理器可以有副作用嗎?

當然!事件處理器是最適合放置副作用的地方。

與渲染函式不同,事件處理器不需要是 純粹的,所以它是個改變某些東西的好地方——例如,響應輸入而改變輸入值,或者響應按鈕按下而改變列表。然而,為了改變某些資訊,你首先需要某種方式來儲存它。在 React 中,這是透過使用 狀態,一個元件的記憶體 來完成的。你將在下一頁學習所有相關知識。

重點回顧

  • 你可以透過將函式作為 prop 傳遞給像 <button> 這樣的元素來處理事件。
  • 事件處理器必須被傳遞,而不是被呼叫! onClick={handleClick},而不是 onClick={handleClick()}
  • 你可以單獨或內嵌定義事件處理器函式。
  • 事件處理器定義在元件內部,因此它們可以訪問 props。
  • 你可以在父元件中宣告一個事件處理器,並將其作為 prop 傳遞給子元件。
  • 你可以使用應用程式特定的名稱定義你自己的事件處理器 props。
  • 事件會向上傳播。在第一個參數上呼叫 e.stopPropagation() 可以防止這種情況。
  • 事件可能會有不需要的預設瀏覽器行為。呼叫 e.preventDefault() 可以防止這種情況。
  • 從子處理器明確呼叫事件處理器 prop 是傳播的一個很好的替代方案。

挑戰 1 2:
修復一個事件處理器

點擊此按鈕應該在白色和黑色之間切換頁面背景。但是,當你點擊它時,什麼也沒有發生。修復這個問題。(不用擔心 handleClick 內部的邏輯——那部分沒有問題。)

export default function LightSwitch() {
  function handleClick() {
    let bodyStyle = document.body.style;
    if (bodyStyle.backgroundColor === 'black') {
      bodyStyle.backgroundColor = 'white';
    } else {
      bodyStyle.backgroundColor = 'black';
    }
  }

  return (
    <button onClick={handleClick()}>
      Toggle the lights
    </button>
  );
}