隨著應用程式的增長,更仔細地思考狀態的組織方式以及資料在元件之間的流動方式將會有所幫助。 冗餘或重複的狀態是常見的錯誤來源。 在本章中,你將學習如何良好地建構狀態、如何保持狀態更新邏輯的可維護性,以及如何在遠距離元件之間共享狀態。
本章內容
使用狀態回應輸入
使用 React 時,你不會直接從程式碼修改 UI。 例如,你不會撰寫「停用按鈕」、「啟用按鈕」、「顯示成功訊息」等指令。 相反地,你將描述你想看到的元件不同視覺狀態(「初始狀態」、「輸入狀態」、「成功狀態」)的 UI,然後觸發狀態變更以回應使用者輸入。 這類似於設計師思考 UI 的方式。
這裡是一個使用 React 建立的測驗表單。 請注意它如何使用 status
狀態變數來決定是否啟用或停用提交按鈕,以及是否顯示成功訊息。
import { useState } from 'react'; export default function Form() { const [answer, setAnswer] = useState(''); const [error, setError] = useState(null); const [status, setStatus] = useState('typing'); if (status === 'success') { return <h1>That's right!</h1> } async function handleSubmit(e) { e.preventDefault(); setStatus('submitting'); try { await submitForm(answer); setStatus('success'); } catch (err) { setStatus('typing'); setError(err); } } function handleTextareaChange(e) { setAnswer(e.target.value); } return ( <> <h2>City quiz</h2> <p> In which city is there a billboard that turns air into drinkable water? </p> <form onSubmit={handleSubmit}> <textarea value={answer} onChange={handleTextareaChange} disabled={status === 'submitting'} /> <br /> <button disabled={ answer.length === 0 || status === 'submitting' }> Submit </button> {error !== null && <p className="Error"> {error.message} </p> } </form> </> ); } function submitForm(answer) { // Pretend it's hitting the network. return new Promise((resolve, reject) => { setTimeout(() => { let shouldError = answer.toLowerCase() !== 'lima' if (shouldError) { reject(new Error('Good guess but a wrong answer. Try again!')); } else { resolve(); } }, 1500); }); }
選擇狀態結構
良好地建構狀態可以決定元件是否易於修改和除錯,或者是否會不斷產生錯誤。 最重要的原則是狀態不應包含冗餘或重複的資訊。 如果存在不必要的狀態,很容易忘記更新它,並導致錯誤!
例如,此表單具有**冗餘**的 fullName
狀態變數
import { useState } from 'react'; export default function Form() { const [firstName, setFirstName] = useState(''); const [lastName, setLastName] = useState(''); const [fullName, setFullName] = useState(''); function handleFirstNameChange(e) { setFirstName(e.target.value); setFullName(e.target.value + ' ' + lastName); } function handleLastNameChange(e) { setLastName(e.target.value); setFullName(firstName + ' ' + e.target.value); } return ( <> <h2>Let’s check you in</h2> <label> First name:{' '} <input value={firstName} onChange={handleFirstNameChange} /> </label> <label> Last name:{' '} <input value={lastName} onChange={handleLastNameChange} /> </label> <p> Your ticket will be issued to: <b>{fullName}</b> </p> </> ); }
你可以移除它,並在元件渲染時計算 fullName
來簡化程式碼
import { useState } from 'react'; export default function Form() { const [firstName, setFirstName] = useState(''); const [lastName, setLastName] = useState(''); const fullName = firstName + ' ' + lastName; function handleFirstNameChange(e) { setFirstName(e.target.value); } function handleLastNameChange(e) { setLastName(e.target.value); } return ( <> <h2>Let’s check you in</h2> <label> First name:{' '} <input value={firstName} onChange={handleFirstNameChange} /> </label> <label> Last name:{' '} <input value={lastName} onChange={handleLastNameChange} /> </label> <p> Your ticket will be issued to: <b>{fullName}</b> </p> </> ); }
這看起來像是一個小改動,但 React 應用程式中的許多錯誤都是透過這種方式修復的。
在元件之間共享狀態
有時,你希望兩個元件的狀態始終一起變更。 為此,請從它們兩者中移除狀態,將其移至它們最近的共同父元件,然後透過屬性將其向下傳遞。 這稱為「提升狀態」,這是撰寫 React 程式碼時最常見的事情之一。
在此範例中,一次只應有一個面板處於活動狀態。 為達成此目的,父元件會持有狀態並指定其子元件的屬性,而不是將活動狀態保留在每個單獨的面板內。
import { useState } from 'react'; export default function Accordion() { const [activeIndex, setActiveIndex] = useState(0); return ( <> <h2>Almaty, Kazakhstan</h2> <Panel title="About" isActive={activeIndex === 0} onShow={() => setActiveIndex(0)} > With a population of about 2 million, Almaty is Kazakhstan's largest city. From 1929 to 1997, it was its capital city. </Panel> <Panel title="Etymology" isActive={activeIndex === 1} onShow={() => setActiveIndex(1)} > The name comes from <span lang="kk-KZ">алма</span>, the Kazakh word for "apple" and is often translated as "full of apples". In fact, the region surrounding Almaty is thought to be the ancestral home of the apple, and the wild <i lang="la">Malus sieversii</i> is considered a likely candidate for the ancestor of the modern domestic apple. </Panel> </> ); } function Panel({ title, children, isActive, onShow }) { return ( <section className="panel"> <h3>{title}</h3> {isActive ? ( <p>{children}</p> ) : ( <button onClick={onShow}> Show </button> )} </section> ); }
保存和重置狀態
重新渲染元件時,React 需要決定要保留(和更新)樹的哪些部分,以及要捨棄或從頭重新建立哪些部分。 在大多數情況下,React 的自動行為運作良好。 根據預設,React 會保留與先前渲染的元件樹「匹配」的樹的部分。
但是,有時這不是你想要的。 在此聊天應用程式中,輸入訊息然後切換收件人不會重置輸入。 這可能會導致使用者不小心將訊息傳送給錯誤的人
import { useState } from 'react'; import Chat from './Chat.js'; import ContactList from './ContactList.js'; export default function Messenger() { const [to, setTo] = useState(contacts[0]); return ( <div> <ContactList contacts={contacts} selectedContact={to} onSelect={contact => setTo(contact)} /> <Chat contact={to} /> </div> ) } const contacts = [ { name: 'Taylor', email: 'taylor@mail.com' }, { name: 'Alice', email: 'alice@mail.com' }, { name: 'Bob', email: 'bob@mail.com' } ];
React 允許您覆蓋預設行為,並透過傳遞不同的 key
(例如 <Chat key={email} />
)來*強制*元件重置其狀態。這告訴 React,如果收件人不同,則應將其視為需要使用新資料(以及輸入框等 UI)從頭開始重新建立的*不同* Chat
元件。現在,即使您渲染相同的元件,切換收件人也會重置輸入欄位。
import { useState } from 'react'; import Chat from './Chat.js'; import ContactList from './ContactList.js'; export default function Messenger() { const [to, setTo] = useState(contacts[0]); return ( <div> <ContactList contacts={contacts} selectedContact={to} onSelect={contact => setTo(contact)} /> <Chat key={to.email} contact={to} /> </div> ) } const contacts = [ { name: 'Taylor', email: 'taylor@mail.com' }, { name: 'Alice', email: 'alice@mail.com' }, { name: 'Bob', email: 'bob@mail.com' } ];
將狀態邏輯提取到 reducer 中
在許多事件處理常式中分散許多狀態更新的元件可能會變得難以管理。在這些情況下,您可以將元件外部的所有狀態更新邏輯整合到一個稱為「reducer」的函式中。您的事件處理常式會變得簡潔,因為它們只指定使用者「動作」。在檔案底部,reducer 函式指定狀態應如何響應每個動作進行更新!
import { useReducer } from 'react'; import AddTask from './AddTask.js'; import TaskList from './TaskList.js'; export default function TaskApp() { const [tasks, dispatch] = useReducer( tasksReducer, initialTasks ); function handleAddTask(text) { dispatch({ type: 'added', id: nextId++, text: text, }); } function handleChangeTask(task) { dispatch({ type: 'changed', task: task }); } function handleDeleteTask(taskId) { dispatch({ type: 'deleted', id: taskId }); } return ( <> <h1>Prague itinerary</h1> <AddTask onAddTask={handleAddTask} /> <TaskList tasks={tasks} onChangeTask={handleChangeTask} onDeleteTask={handleDeleteTask} /> </> ); } function tasksReducer(tasks, action) { switch (action.type) { case 'added': { return [...tasks, { id: action.id, text: action.text, done: false }]; } case 'changed': { return tasks.map(t => { if (t.id === action.task.id) { return action.task; } else { return t; } }); } case 'deleted': { return tasks.filter(t => t.id !== action.id); } default: { throw Error('Unknown action: ' + action.type); } } } let nextId = 3; const initialTasks = [ { id: 0, text: 'Visit Kafka Museum', done: true }, { id: 1, text: 'Watch a puppet show', done: false }, { id: 2, text: 'Lennon Wall pic', done: false } ];
使用 Context 深度傳遞資料
通常,您會透過 props 將資訊從父元件傳遞到子元件。但是,如果您需要透過許多元件傳遞某些 prop,或者許多元件需要相同的資訊,則傳遞 props 可能會變得不方便。Context 允許父元件將某些資訊提供給其下方樹狀結構中的任何元件(無論深度如何),而無需透過 props 明確傳遞。
在這裡,Heading
元件透過「詢問」最接近的 Section
其級別來決定其標題級別。每個 Section
透過詢問父 Section
並在其上加一來追蹤其自身的級別。每個 Section
都會向其下方的所有元件提供資訊,而無需傳遞 props—它是透過 Context 來實現的。
import Heading from './Heading.js'; import Section from './Section.js'; export default function Page() { return ( <Section> <Heading>Title</Heading> <Section> <Heading>Heading</Heading> <Heading>Heading</Heading> <Heading>Heading</Heading> <Section> <Heading>Sub-heading</Heading> <Heading>Sub-heading</Heading> <Heading>Sub-heading</Heading> <Section> <Heading>Sub-sub-heading</Heading> <Heading>Sub-sub-heading</Heading> <Heading>Sub-sub-heading</Heading> </Section> </Section> </Section> </Section> ); }
使用 Reducer 和 Context 擴展
Reducer 允許您整合元件的狀態更新邏輯。Context 允許您將資訊深入傳遞到其他元件。您可以將 Reducer 和 Context 結合起來管理複雜螢幕的狀態。
使用這種方法,具有複雜狀態的父元件可以使用 Reducer 管理它。樹狀結構中任何深處的其他元件都可以透過 Context 讀取其狀態。它們還可以發送動作來更新該狀態。
import AddTask from './AddTask.js'; import TaskList from './TaskList.js'; import { TasksProvider } from './TasksContext.js'; export default function TaskApp() { return ( <TasksProvider> <h1>Day off in Kyoto</h1> <AddTask /> <TaskList /> </TasksProvider> ); }