2.6 表單
在有交互的Web應用中,表單是必不可少的。但是,和其他元素相比,表單元素在React中的工作方式存在一些不同。像div、p、span等非表單元素只需根據組件的屬性或狀態進行渲染即可,但表單元素自身維護一些狀態,而這些狀態默認情況下是不受React控制的。例如,input元素會根據用戶的輸入自動改變顯示的內容,而不是從組件的狀態中獲取顯示的內容。我們稱這類狀態不受React控制的表單元素為非受控組件。在React中,狀態的修改必須通過組件的state,非受控組件的行為顯然有悖于這一原則。為了讓表單元素狀態的變更也能通過組件的state管理,React采用受控組件的技術達到這一目的。
2.6.1 受控組件
如果一個表單元素的值是由React來管理的,那么它就是一個受控組件。React組件渲染表單元素,并在用戶和表單元素發生交互時控制表單元素的行為,從而保證組件的state成為界面上所有元素狀態的唯一來源。對于不同的表單元素,React的控制方式略有不同,下面我們就來看一下三類常用表單元素的控制方式。
1.文本框
文本框包含類型為text的input元素和textarea元素。它們受控的主要原理是,通過表單元素的value屬性設置表單元素的值,通過表單元素的onChange事件監聽值的變化,并將變化同步到React組件的state中。下面是一個例子。

用戶名和密碼兩個表單元素的值是從組件的state中獲取的,當用戶更改表單元素的值時,onChange事件會被觸發,對應的handleChange處理函數會把變化同步到組件的state,新的state又會觸發表單元素重新渲染,從而實現對表單元素狀態的控制。
這個例子還包含一個處理多個表單元素的技巧:通過為兩個input元素分別指定name屬性,使用同一個函數handleChange處理元素值的變化,在處理函數中根據元素的name屬性區分事件的來源。這樣的寫法顯然比為每一個input元素指定一個處理函數簡潔得多。
textarea的使用方式和input幾乎一致,這里不再贅述。
2.列表
列表select元素是最復雜的表單元素,它可以用來創建一個下拉列表:

通過指定selected屬性可以定義哪一個選項(option)處于選中狀態,所以上面的例子中,Mobx這一選項是列表的初始值,處于選中狀態。在React中,對select的處理方式有所不同,它通過在select上定義value屬性來決定哪一個option元素處于選中狀態。這樣,對select的控制只需要在select這一個元素上修改即可,而不需要關注option元素。下面是一個例子:

3.復選框和單選框
復選框是類型為checkbox的input元素,單選框是類型為radio的input元素,它們的受控方式不同于類型為text的input元素。通常,復選框和單選框的值是不變的,需要改變的是它們的checked狀態,因此React控制的屬性不再是value屬性,而是checked屬性。例如:


上面的例子中,input的value是不變的,onChange事件改變的是input的checked屬性。單選框的用法和復選框相似,讀者可自行嘗試使用。
下面為BBS項目添加表單元素,讓每一個帖子的標題支持編輯功能。本節項目源代碼的目錄為/chapter-02/bbs-components-form。修改后的PostItem如下:



當點擊編輯狀態的button時,帖子的標題會使用textarea展示,此時標題處于可編輯狀態,當再次點擊button時,會執行保存操作,PostItem通過onSave屬性調用父組件PostList的handleSave方法,將更新后的Post(標題和時間)保存到PostList的state中。PostList中的修改如下:

2.6.2 非受控組件
使用受控組件雖然保證了表單元素的狀態也由React統一管理,但需要為每個表單元素定義onChange事件的處理函數,然后把表單狀態的更改同步到React組件的state,這一過程是比較煩瑣的,一種可替代的解決方案是使用非受控組件。非受控組件指表單元素的狀態依然由表單元素自己管理,而不是交給React組件管理。使用非受控組件需要有一種方式可以獲取到表單元素的值,React中提供了一個特殊的屬性ref,用來引用React組件或DOM元素的實例,因此我們可以通過為表單元素定義ref屬性獲取元素的值。例如:

ref的值是一個函數,這個函數會接收當前元素作為參數,即例子中的input參數指向的是當前元素。在函數中,我們把input賦值給了this.input,進而可以在組件的其他地方通過this.input獲取這個元素。
在使用非受控組件時,我們常常需要為相應的表單元素設置默認值,但是無法通過表單元素的value屬性設置,因為非受控組件中,React無法控制表單元素的value屬性,這也就意味著一旦在非受控組件中定義了value屬性的值,就很難保證后續表單元素的值的正確性。這種情況下,我們可以使用defaultValue屬性指定默認值:

上面的例子,defaultValue設置的默認值為something,而后續值的更改則由自己控制。類似地,select元素和textarea元素也支持通過defaultValue設置默認值,<input type="checkbox">和<input type="radio">則支持通過defaultChecked屬性設置默認值。
非受控組件看似簡化了操作表單元素的過程,但這種方式破壞了React對組件狀態管理的一致性,往往容易出現不容易排查的問題,因此非特殊情況下,不建議大家使用。