新增互動性
螢幕上的某些內容會根據使用者輸入進行更新。例如,點擊圖片庫會切換目前顯示的圖片。在 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 的過程分為三個步驟
- 觸發渲染(將顧客的點餐送到廚房)
- 渲染元件(在廚房準備點餐)
- 提交到 DOM(將點餐放在餐桌上)
觸發 渲染 提交
圖示作者 Rachel Lee Nabors
狀態作為快照
與普通的 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> ); }
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> </> ) }
更新狀態中的物件
狀態可以儲存任何種類的 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": {} }
更新狀態中的陣列
陣列是另一種可以儲存在狀態中,並且應該被視為唯讀的可修改 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": {} }