鐵人賽過半,終於進入到處理表單,官方文件顯示我們已經從 Beginner 進入 Intermediate 的程度,連續地寫作比想像中的還要疲勞,作業 BGM 也從後搖變成身心科會播的那種治癒系鋼琴聲,但還是得繼續前進,今天會提到:
React 使用的是聲明式 UI,根據畫面的視覺與功能直接聲明「想呈現的事情」而 React 會負責更新 UI。這個方法跟我們在學習 JavaScript 時所熟悉的命令式,直接使用 DOM 去操作不同,在命令式時,我們必須明確地根據「現狀」去下指令,就像是幫在開車的司機導航一樣,告訴司機怎麼走。「先直走、再左轉、右轉」,但司機並不知道到底要去哪,所有的指令依賴上一個動作,也就是去描述「接下來」該怎麼做。而使用聲明式則不同,聲明式就像直接告訴司機我們要去的目的地,由司機載我們過去,這樣的方法也跟設計師在設計的邏輯比較接近,如同說明「我希望 UI 看起來/用起來是這樣。」
在設計聲明式 UI 的第一個步驟是先區分我們可能會有哪些狀態(state),可以透過 mocking 的技巧,在尚未編寫深入的邏輯之前,先模擬過各種狀態的呈現。譬如一個 input form,可能會有以下的狀態「空白」、「輸入中」、「提交中」、「成功」、「錯誤」。
export default function Form({
// Try 'submitting', 'error', 'success':
status = "empty",
}) {
if (status === "success") {
return <h1>That's right!</h1>;
}
return (
<>
<h2>City quiz</h2>
<p>
In which city is there a billboard that turns air into drinkable water?
</p>
<form>
<textarea disabled={status === "submitting"} />
<br />
<button disabled={status === "empty" || status === "submitting"}>
Submit
</button>
{status === "error" && (
<p className="Error">Good guess but a wrong answer. Try again!</p>
)}
</form>
</>
);
}
這個表單會依據不同的狀態(status)顯示不同的內容。若我們變更
status = "empty";
改變為「success」或是其他的狀態則會出現不同的結果。例如:
其他狀態亦同。
完成了模擬(mocking)即可進入下一階段來處理渲染。
觸發渲染的形式可以分成二種「人為輸入(Human inputs)」、「電腦輸入(Computer inputs)」。例如:
人為 | 電腦 |
---|---|
• 文字輸入 | • 失敗或成功的網路回應 |
• 點擊提交按鈕 | • 圖像載入 |
• 點擊連結、按鈕 | • 計時器完成 |
不論哪一種情況都必須要設定 state variable(也就是之前練習的 useState) 來觸發渲染。
React 官方文件也提供了一個流程圖來描述這樣的過程:
(原始圖片連結:https://react.dev/learn/reacting-to-input-with-state#step-2-determine-what-triggers-those-state-changes)_
大原則是維持簡潔,每一種狀態都可以視為一片可動性的片段(moving piece),所以越簡潔越能夠降低 bug 的發生率。安排狀態有的時候需要時間去實驗,但基本款的思考方法是確保所有可能的視覺狀態都有對應的狀態。例如剛剛的例子,安排上就會變成這樣:
const [isEmpty, setIsEmpty] = useState(true);
const [isTyping, setIsTyping] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
const [isError, setIsError] = useState(false);
在完成初步的狀態安排,可以思考如何重構(refactoring )讓元件更加易懂、減少多餘的複製。我們應該確保應用程式的狀態在任何時候都能夠正確地反映出我們希望用戶看到的 UI,而不應該呈現不一致或不合法的 UI。譬如以下的情境:
那我們可以怎麼去檢查呢?嘗試問問以下的問題:
這個 state 是否有矛盾的地方?
例如 isTyping
和 isSubmitting
同時為真的情況表示狀態變數沒有受到足夠的約束,導致了無效的組合。可以將這些狀態合併成一個具有限制性的 status 變數。
這個概念是否在其他 state 也表達過了(重複的狀態變數)?
例如isEmpty
和 isTyping
同時為真。這裡的建議是避免使用過多的重複狀態變數,因為這可能導致狀態不一致引發錯誤。取而代之,你可以通過單一的狀態變數來表示相關訊息,例如 answer.length === 0
可以替代 isEmpty
。
是否資訊能夠透過其他的 state 的相反狀態來獲得呢?
isError 變數好像也可拿掉,因為可以透過檢查 error !== null
來判斷是否存在錯誤。
經過這樣的過程,我們的 useState 可以寫成以下這樣子:
const [answer, setAnswer] = useState("");
const [error, setError] = useState(null);
const [status, setStatus] = useState("typing"); // 'typing', 'submitting', or 'success'
這個部分也就是我們之前學習過的部分,經過五個步驟後完整的程式碼就可以是以下這樣:
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);
});
}
今天的內容比較偏向觀念上的學習,透過這五個步驟讓我們更了解了聲明式 UI 的思考方式及流程。