使用狀態響應輸入
React 提供了一種宣告式的方法來操作 UI。與直接操作 UI 的各個部分不同,你可以描述你的元件可能處於的不同狀態,並根據使用者輸入在它們之間切換。這類似於設計師思考 UI 的方式。
您將學習
- 宣告式 UI 程式設計與命令式 UI 程式設計有何不同
- 如何枚舉你的元件可能處於的不同視覺狀態
- 如何從程式碼觸發不同視覺狀態之間的變化
宣告式 UI 與命令式 UI 的比較
當你設計 UI 互動時,你可能會思考 UI 如何變化以響應使用者的操作。考慮一個讓使用者提交答案的表單:
- 當你在表單中輸入內容時,「提交」按鈕會變為啟用狀態。
- 當你按下「提交」時,表單和按鈕都會變為停用狀態,並且會出現一個載入指示器。
- 如果網路請求成功,表單會被隱藏,並且會出現「感謝您」的訊息。
- 如果網路請求失敗,會出現錯誤訊息,並且表單會再次變為啟用狀態。
在命令式程式設計中,上述內容直接對應於你如何實現互動。你必須根據剛剛發生的事情,編寫確切的指令來操作 UI。換一種方式思考:想像你坐在車裡,旁邊有人開車,你一步一步地告訴他們該往哪裡轉。

他們不知道你想去哪裡,他們只是遵循你的命令。(如果你給錯了方向,你最終會到達錯誤的地方!)這被稱為命令式,因為你必須「命令」每個元素,從載入指示器到按鈕,告訴電腦如何更新 UI。
在這個命令式 UI 程式設計的例子中,表單是沒有使用 React 建構的。它只使用了瀏覽器的DOM:
以命令式方式操作 UI 對於孤立的例子來說效果不錯,但在更複雜的系統中,管理起來會變得指數級地困難。想像一下更新一個充滿像這樣的不同表單的頁面。添加一個新的 UI 元素或新的互動將需要仔細檢查所有現有程式碼,以確保你沒有引入錯誤(例如,忘記顯示或隱藏某些東西)。
React 就是為了解決這個問題而建立的。
在 React 中,你不直接操作 UI——這意味著你不直接啟用、停用、顯示或隱藏元件。相反,你宣告你想要顯示什麼,然後 React 會找出如何更新 UI。想像一下,你上了一輛計程車,告訴司機你想去哪裡,而不是告訴他們確切在哪裡轉彎。司機的工作就是把你送到那裡,他們甚至可能知道一些你沒有考慮到的捷徑!

以宣告式方式思考 UI
你已經看到了上面如何以命令式方式實現一個表單。為了更好地理解如何在 React 中思考,你將在下面逐步重新在 React 中實現這個 UI:
- 識別您組件的不同視覺狀態
- 確定觸發這些狀態變化的因素
- 使用
useState在記憶體中表示狀態 - 移除任何非必要的狀態變數
- 連接事件處理器來設定狀態
步驟 1:識別您組件的不同視覺狀態
在電腦科學中,您可能聽過「狀態機」處於數個「狀態」中的一個。如果您與設計師合作,您可能看過不同「視覺狀態」的模型。React 處於設計與電腦科學的交集,因此這兩種想法都是靈感來源。
您需要視覺化使用者可能看到的所有不同 UI「狀態」:
- 空白:表單有一個停用的「提交」按鈕。
- 輸入中:表單有一個啟用的「提交」按鈕。
- 提交中:表單完全停用。顯示載入動畫。
- 成功:顯示「感謝您」訊息而非表單。
- 錯誤:與「輸入中」狀態相同,但額外顯示錯誤訊息。
就像設計師一樣,您會希望在加入邏輯之前為不同狀態「製作模型」或建立「模型」。例如,這裡是僅針對表單視覺部分的模型。此模型由一個名為status、預設值為'empty'的屬性控制:
您可以隨意命名該屬性,名稱並不重要。嘗試將status = 'empty'編輯為status = 'success'以查看成功訊息出現。模型化讓您可以在連接任何邏輯之前快速迭代 UI。以下是同一個組件更完整的原型,仍然由status屬性「控制」:
步驟 2:確定觸發這些狀態變化的因素
您可以響應兩種輸入來觸發狀態更新:
- 人類輸入,例如點擊按鈕、在欄位中輸入、導覽連結。
- 電腦輸入,例如網路回應到達、計時器完成、圖片載入。


插圖由Rachel Lee Nabors繪製
在這兩種情況下,您都必須設定狀態變數來更新 UI。對於您正在開發的表單,您將需要響應幾個不同的輸入來改變狀態:
- 變更文字輸入(人為操作)應將其從空白狀態切換到輸入中狀態或反之,取決於文字方塊是否為空。
- 點擊提交按鈕(人為操作)應將其切換到提交中狀態。
- 成功的網路回應(電腦)應將其切換到成功狀態。
- 失敗的網路回應(電腦)應將其切換到錯誤狀態並顯示對應的錯誤訊息。
注意
請注意,人為輸入通常需要事件處理器!
為了幫助視覺化這個流程,可以嘗試在紙上將每個狀態畫成一個標記的圓圈,並將兩個狀態之間的每次變更畫成一個箭頭。你可以用這種方式勾勒出許多流程,並在實作之前就找出錯誤。


表單狀態
步驟 3:使用 在記憶體中表示狀態useState
接下來,你需要使用 useState在記憶體中表示元件的視覺狀態。簡單性是關鍵:每個狀態片段都是一個「移動的部件」,而你希望「移動的部件」越少越好。 越複雜就越容易出錯!
從絕對必須存在的狀態開始。例如,你需要儲存輸入的answer,以及error(如果存在的話)來儲存最後的錯誤:
然後,你需要一個狀態變數來表示你想要顯示的視覺狀態是哪一個。通常有多種方式可以在記憶體中表示這一點,因此你需要進行實驗。
如果你一時想不出最佳方法,可以先加入足夠的狀態,以確保涵蓋所有可能的視覺狀態:
你的第一個想法可能不是最好的,但沒關係——重構狀態是這個過程的一部分!
步驟 4:移除任何非必要的狀態變數
你希望避免狀態內容的重複,以便只追蹤必要的部分。花一點時間重構你的狀態結構,將使你的元件更容易理解、減少重複,並避免意外的含義。你的目標是防止記憶體中的狀態不代表任何你希望使用者看到的有效使用者介面的情況。(例如,你絕不希望同時顯示錯誤訊息並停用輸入,否則使用者將無法更正錯誤!)
以下是一些你可以詢問關於狀態變數的問題:
- 這個狀態會導致矛盾嗎? 例如,
isTyping和isSubmitting不可能同時為true。矛盾通常意味著狀態的約束不夠。兩個布林值有四種可能的組合,但只有三種對應有效的狀態。為了移除「不可能」的狀態,你可以將它們合併成一個status,它必須是三個值之一:'typing'、'submitting'或'success'。 - 相同的資訊是否已經存在於另一個狀態變數中?另一個矛盾:
isEmpty和isTyping不可能同時為true。將它們設為獨立的狀態變數,會讓它們有不同步並導致錯誤的風險。幸運的是,你可以移除isEmpty,改為檢查answer.length === 0。 - 你能從另一個狀態變數的反面得到相同的資訊嗎?
isError並不需要,因為你可以改為檢查error !== null。
經過這次清理,你剩下 3 個(從 7 個減少!)必要的狀態變數:
你知道它們是必要的,因為移除任何一個都會破壞功能。
步驟 5:將事件處理器連接到狀態設定
最後,建立更新狀態的事件處理器。以下是完整的最終表單,所有事件處理器都已連接:
雖然這段程式碼比原始的指令式範例長,但它穩健得多。將所有互動表達為狀態變化,讓你之後可以引入新的視覺狀態而不破壞現有的狀態。它也讓你可以改變每個狀態下應該顯示的內容,而不需要改變互動本身的邏輯。
總結
- 宣告式程式設計意味著為每個視覺狀態描述 UI,而不是微觀管理 UI(指令式)。
- 開發元件時:
- 識別其所有視覺狀態。
- 確定狀態變化的人為和電腦觸發因素。
- 使用
useState建模狀態。 - 移除非必要的狀態以避免錯誤和矛盾。
- 將事件處理器連接到狀態設定。
Try out some challenges
Challenge 1 of 3:Add and remove a CSS class #
Make it so that clicking on the picture removes the background--active CSS class from the outer <div>, but adds the picture--active class to the <img>. Clicking the background again should restore the original CSS classes.
Visually, you should expect that clicking on the picture removes the purple background and highlights the picture border. Clicking outside the picture highlights the background, but removes the picture border highlight.
