新增互動性

螢幕上的某些內容會根據使用者輸入進行更新。例如,點擊圖片庫會切換目前顯示的圖片。在 React 中,隨著時間變化的資料稱為狀態。您可以將狀態新增到任何元件,並根據需要更新它。在本章中,您將學習如何編寫處理互動、更新其狀態並隨著時間推移顯示不同輸出的元件。

回應事件

React 允許您將事件處理器新增到您的 JSX。事件處理器是您自己的函式,將在回應使用者互動(如點擊、懸停、聚焦表單輸入等)時觸發。

<button> 之類的內建元件僅支援內建的瀏覽器事件,例如 onClick。但是,您也可以建立自己的元件,並為其事件處理器 props 指定您喜歡的任何應用程式特定名稱。

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>
  );
}

準備好學習這個主題了嗎?

閱讀回應事件 以了解如何新增事件處理器。

閱讀更多

狀態:元件的記憶

元件通常需要根據互動結果更改螢幕上的內容。在表單中輸入文字應更新輸入欄位,在圖片輪播中按一下「下一張」應更改顯示的圖片,按一下「購買」會將產品放入購物車。元件需要「記住」一些事情:目前的輸入值、目前的圖片、購物車。在 React 中,這種特定於元件的記憶稱為狀態

您可以使用 useState Hook 將狀態新增到元件。 Hooks 是特殊的函式,可讓您的元件使用 React 功能(狀態是其中一項功能)。 useState Hook 可讓您宣告狀態變數。它採用初始狀態並傳回一對值:目前的狀態和一個狀態設定器函式,可讓您更新它。

const [index, setIndex] = useState(0);
const [showMore, setShowMore] = useState(false);

以下是圖片庫如何在點擊時使用和更新狀態

import { useState } from 'react';
import { sculptureList } from './data.js';

export default function Gallery() {
  const [index, setIndex] = useState(0);
  const [showMore, setShowMore] = useState(false);
  const hasNext = index < sculptureList.length - 1;

  function handleNextClick() {
    if (hasNext) {
      setIndex(index + 1);
    } else {
      setIndex(0);
    }
  }

  function handleMoreClick() {
    setShowMore(!showMore);
  }

  let sculpture = sculptureList[index];
  return (
    <>
      <button onClick={handleNextClick}>
        Next
      </button>
      <h2>
        <i>{sculpture.name} </i>
        by {sculpture.artist}
      </h2>
      <h3>
        ({index + 1} of {sculptureList.length})
      </h3>
      <button onClick={handleMoreClick}>
        {showMore ? 'Hide' : 'Show'} details
      </button>
      {showMore && <p>{sculpture.description}</p>}
      <img
        src={sculpture.url}
        alt={sculpture.alt}
      />
    </>
  );
}

準備好學習這個主題了嗎?

閱讀狀態:元件的記憶 以了解如何記住一個值並在互動時更新它。

閱讀更多

渲染和提交

在您的元件顯示在螢幕上之前,它們必須由 React 進行渲染。了解此過程中的步驟將幫助您思考程式碼的執行方式並解釋其行為。

想像一下,您的元件是廚房裡的廚師,用食材組裝美味的菜餚。在這種情況下,React 是服務生,負責接收顧客的點餐並將其送達。這個請求和提供 UI 的過程分為三個步驟

  1. 觸發渲染(將顧客的點餐送到廚房)
  2. 渲染元件(在廚房準備點餐)
  3. 提交到 DOM(將點餐放在餐桌上)
  1. React as a server in a restaurant, fetching orders from the users and delivering them to the Component Kitchen.
    觸發
  2. The Card Chef gives React a fresh Card component.
    渲染
  3. React delivers the Card to the user at their table.
    提交

圖示作者 Rachel Lee Nabors

準備好學習這個主題了嗎?

閱讀渲染和提交 以了解 UI 更新的生命週期。

閱讀更多

狀態作為快照

與普通的 JavaScript 變數不同,React 狀態更像是快照。設定它不會更改您已有的狀態變數,而是觸發重新渲染。這一開始可能會令人驚訝!

console.log(count); // 0
setCount(count + 1); // Request a re-render with 1
console.log(count); // Still 0!

這種行為可以幫助你避免一些微妙的錯誤。這裡有一個小的聊天應用程式。試著猜猜看,如果你先按下「發送」,*然後*再將收件人改成 Bob,五秒鐘後在 alert 中會出現誰的名字?

import { useState } from 'react';

export default function Form() {
  const [to, setTo] = useState('Alice');
  const [message, setMessage] = useState('Hello');

  function handleSubmit(e) {
    e.preventDefault();
    setTimeout(() => {
      alert(`You said ${message} to ${to}`);
    }, 5000);
  }

  return (
    <form onSubmit={handleSubmit}>
      <label>
        To:{' '}
        <select
          value={to}
          onChange={e => setTo(e.target.value)}>
          <option value="Alice">Alice</option>
          <option value="Bob">Bob</option>
        </select>
      </label>
      <textarea
        placeholder="Message"
        value={message}
        onChange={e => setMessage(e.target.value)}
      />
      <button type="submit">Send</button>
    </form>
  );
}

準備好學習這個主題了嗎?

閱讀狀態快照 (State as a Snapshot) 了解為什麼在事件處理函式中,狀態看起來是「固定」且不變的。

閱讀更多

狀態更新佇列

這個元件有錯誤:點擊「+3」只會將分數增加一次。

import { useState } from 'react';

export default function Counter() {
  const [score, setScore] = useState(0);

  function increment() {
    setScore(score + 1);
  }

  return (
    <>
      <button onClick={() => increment()}>+1</button>
      <button onClick={() => {
        increment();
        increment();
        increment();
      }}>+3</button>
      <h1>Score: {score}</h1>
    </>
  )
}

狀態快照 (State as a Snapshot) 解釋了為什麼會發生這種情況。設定狀態會請求重新渲染,但不會改變已在執行的程式碼中的狀態。因此,在您呼叫 setScore(score + 1) 之後,score 仍然是 0

console.log(score); // 0
setScore(score + 1); // setScore(0 + 1);
console.log(score); // 0
setScore(score + 1); // setScore(0 + 1);
console.log(score); // 0
setScore(score + 1); // setScore(0 + 1);
console.log(score); // 0

您可以透過在設定狀態時傳遞一個 *更新函式* 來解決這個問題。請注意,將 setScore(score + 1) 替換為 setScore(s => s + 1) 如何修復「+3」按鈕。這讓您可以將多個狀態更新排入佇列。

import { useState } from 'react';

export default function Counter() {
  const [score, setScore] = useState(0);

  function increment() {
    setScore(s => s + 1);
  }

  return (
    <>
      <button onClick={() => increment()}>+1</button>
      <button onClick={() => {
        increment();
        increment();
        increment();
      }}>+3</button>
      <h1>Score: {score}</h1>
    </>
  )
}

準備好學習這個主題了嗎?

閱讀狀態更新佇列 (Queueing a Series of State Updates) 了解如何將一系列狀態更新排入佇列。

閱讀更多

更新狀態中的物件

狀態可以儲存任何種類的 JavaScript 值,包括物件。但是你不應該直接修改儲存在 React 狀態中的物件和陣列。相反地,當您想要更新物件和陣列時,您需要建立一個新的(或複製現有的),然後將狀態更新為使用該副本。

通常,您會使用 ... 展開語法來複製您想要修改的物件和陣列。例如,更新巢狀物件可能看起來像這樣

import { useState } from 'react';

export default function Form() {
  const [person, setPerson] = useState({
    name: 'Niki de Saint Phalle',
    artwork: {
      title: 'Blue Nana',
      city: 'Hamburg',
      image: 'https://i.imgur.com/Sd1AgUOm.jpg',
    }
  });

  function handleNameChange(e) {
    setPerson({
      ...person,
      name: e.target.value
    });
  }

  function handleTitleChange(e) {
    setPerson({
      ...person,
      artwork: {
        ...person.artwork,
        title: e.target.value
      }
    });
  }

  function handleCityChange(e) {
    setPerson({
      ...person,
      artwork: {
        ...person.artwork,
        city: e.target.value
      }
    });
  }

  function handleImageChange(e) {
    setPerson({
      ...person,
      artwork: {
        ...person.artwork,
        image: e.target.value
      }
    });
  }

  return (
    <>
      <label>
        Name:
        <input
          value={person.name}
          onChange={handleNameChange}
        />
      </label>
      <label>
        Title:
        <input
          value={person.artwork.title}
          onChange={handleTitleChange}
        />
      </label>
      <label>
        City:
        <input
          value={person.artwork.city}
          onChange={handleCityChange}
        />
      </label>
      <label>
        Image:
        <input
          value={person.artwork.image}
          onChange={handleImageChange}
        />
      </label>
      <p>
        <i>{person.artwork.title}</i>
        {' by '}
        {person.name}
        <br />
        (located in {person.artwork.city})
      </p>
      <img
        src={person.artwork.image}
        alt={person.artwork.title}
      />
    </>
  );
}

如果在程式碼中複製物件變得繁瑣,您可以使用像是 Immer 這樣的函式庫來減少重複的程式碼

{
  "dependencies": {
    "immer": "1.7.3",
    "react": "latest",
    "react-dom": "latest",
    "react-scripts": "latest",
    "use-immer": "0.5.1"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test --env=jsdom",
    "eject": "react-scripts eject"
  },
  "devDependencies": {}
}

準備好學習這個主題了嗎?

閱讀更新狀態中的物件 (Updating Objects in State) 了解如何正確地更新物件。

閱讀更多

更新狀態中的陣列

陣列是另一種可以儲存在狀態中,並且應該被視為唯讀的可修改 JavaScript 物件。就像物件一樣,當您想要更新儲存在狀態中的陣列時,您需要建立一個新的(或複製現有的),然後將狀態設定為使用新的陣列

import { useState } from 'react';

const initialList = [
  { id: 0, title: 'Big Bellies', seen: false },
  { id: 1, title: 'Lunar Landscape', seen: false },
  { id: 2, title: 'Terracotta Army', seen: true },
];

export default function BucketList() {
  const [list, setList] = useState(
    initialList
  );

  function handleToggle(artworkId, nextSeen) {
    setList(list.map(artwork => {
      if (artwork.id === artworkId) {
        return { ...artwork, seen: nextSeen };
      } else {
        return artwork;
      }
    }));
  }

  return (
    <>
      <h1>Art Bucket List</h1>
      <h2>My list of art to see:</h2>
      <ItemList
        artworks={list}
        onToggle={handleToggle} />
    </>
  );
}

function ItemList({ artworks, onToggle }) {
  return (
    <ul>
      {artworks.map(artwork => (
        <li key={artwork.id}>
          <label>
            <input
              type="checkbox"
              checked={artwork.seen}
              onChange={e => {
                onToggle(
                  artwork.id,
                  e.target.checked
                );
              }}
            />
            {artwork.title}
          </label>
        </li>
      ))}
    </ul>
  );
}

如果在程式碼中複製陣列變得繁瑣,您可以使用像是 Immer 這樣的函式庫來減少重複的程式碼

{
  "dependencies": {
    "immer": "1.7.3",
    "react": "latest",
    "react-dom": "latest",
    "react-scripts": "latest",
    "use-immer": "0.5.1"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test --env=jsdom",
    "eject": "react-scripts eject"
  },
  "devDependencies": {}
}

準備好學習這個主題了嗎?

閱讀更新狀態中的陣列 (Updating Arrays in State) 了解如何正確地更新陣列。

閱讀更多

接下來呢?

前往 回應事件 (Responding to Events) 開始逐頁閱讀本章!

或者,如果您已經熟悉這些主題,為什麼不閱讀關於 狀態管理 (Managing State) 的內容呢?