翻譯 | 玩轉 React 表單 —— 受控組件詳解

  • 原文地址:React.js Forms: Controlled Components
  • 原文作者:Loren Stewart
  • 譯者:小 B0Y
  • 校對者:珂珂君

本文涵蓋以下受控組件:

  • 文本輸入框
  • 數字輸入框
  • 單選框
  • 複選框
  • 文本域
  • 下拉選擇框

同時也包含:

  • 表單數據的清除和重置
  • 表單數據的提交
  • 表單校驗

點擊這裡直接查看示例代碼。 查看示例。 請在運行示例時打開瀏覽器的控制台。

介紹

在學習 React.js 時我遇到了一個問題,那就是很難找到受控組件的真實示例。受控文本輸入框的例子倒是很豐富,但複選框、單選框、下拉選擇框的例子卻不盡人意。

本文列舉了真實的受控表單組件示例,要是我在學習 React 的時候早點發現這些示例就好了。除了日期和時間輸入框需要另開篇幅詳細討論,文中列舉了所有的表單元素。

有時候,為了減少開發時間,有時候人們很容易為了一些東西(譬如表單元素)引入一個庫。而對於表單,我發現當需要添加自定義行為或表單校驗時,使用庫會讓事情變得更複雜。不過一旦掌握合適的 React 模式,你會發現構建表單組件並非難事,並且有些東西完全可以自己動手,豐衣足食。請把本文的示例代碼當作你創建表單組件的起點或靈感之源。

除了提供單獨的組件代碼,我還將這些組件放進表單中,方便你理解子組件如何更新父組件 state ,以及接下來父組件如何通過 props(單向數據流)更新子組件。

注意:本表單示例由很贊的 create-react-app 構建配置生成,如果你還沒有安裝該構建配置,我強烈推薦你安裝一下(npm install -g create-react-app)。目前這是搭建 React 應用最簡單的方式。

什麼是受控組件?

受控組件有兩個特點:

  1. 受控組件提供方法,讓我們在每次 onChange 事件發生時控制它們的數據,而不是一次性地獲取表單數據(例如用戶點提交按鈕時)。「被控制「 的表單數據保存在 state 中(在本文示例中,是父組件或容器組件的 state)。

    (譯註:這裡作者的意思是通過受控組件, 可以跟蹤用戶操作表單時的數據,從而更新容器組件的 state ,再單向渲染表單元素 UI。如果不使用受控組件,在用戶實時操作表單時,比如在輸入框輸入文本時,不會同步到容器組件的 state,雖然能同步輸入框本身的 value,但與容器組件的 state 無關,因此容器組件只能在某一時間,比如提表單時一次性地拿到(通過 refs 或者選擇器)表單數據,而難以跟蹤)
  2. 受控組件的展示數據是其父組件通過 props 傳遞下來的。

這個單向循環 —— (數據)從(1)子組件輸入到(2)父組件的 state,接著(3)通過 props 回到子組件,就是 React.js 應用架構中單向數據流的含義。

表單結構

我們的頂級組件叫做 App,這是它的代碼:

import React, { Component } from react; nimport ../node_modules/spectre.css/dist/spectre.min.css; nimport ./styles.css; nimport FormContainer from ./containers/FormContainer;nnclass App extends Component { n render() {n return (n <div className="container">n <div className="columns">n <div className="col-md-9 centered">n <h3>React.js Controlled Form Components</h3>n <FormContainer />n </div>n </div>n </div>n );n }n}nnexport default App; n

App 只負責渲染 index.html 頁面。整個 App 組件最有趣的部分是 13 行,FormContainer 組件。

插曲: 容器(智能)組件 VS 普通(木偶)組件

是時候提及一下容器(智能)組件和普通(木偶)組件了。容器組件包含業務邏輯,它會發起數據請求或進行其他業務操作。普通組件則從它的父(容器)組件接收數據。木偶組件有可能觸發更新 state (譯註:容器組件的 state)這類邏輯行為,但它僅通過從父(容器)組件傳入的方法來達到該目的。

注意: 雖然在我們的表單應用里父組件就是容器組件,但我要強調,並非所有的父組件都是容器組件。木偶組件嵌套木偶組件也是可以的。

回到應用結構

FormContainer 組件包含了表單元素組件,它在生命周期鉤子方法 componentDidMount 里請求數據,此外還包含更新表單應用 state 的邏輯行為。在下面的預覽代碼里,我移除了表單元素的 props 和 change 事件處理方法,這樣看起來更簡潔清晰(拉到文章底部,可以看到完整代碼)。

import React, {Component} from react; nimport CheckboxOrRadioGroup from ../components/CheckboxOrRadioGroup; nimport SingleInput from ../components/SingleInput; nimport TextArea from ../components/TextArea; nimport Select from ../components/Select;nnclass FormContainer extends Component { n constructor(props) {n super(props);n this.handleFormSubmit = this.handleFormSubmit.bind(this);n this.handleClearForm = this.handleClearForm.bind(this);n }n componentDidMount() {n fetch(./fake_db.json)n .then(res => res.json())n .then(data => {n this.setState({n ownerName: data.ownerName,n petSelections: data.petSelections,n selectedPets: data.selectedPets,n ageOptions: data.ageOptions,n ownerAgeRangeSelection: data.ownerAgeRangeSelection,n siblingOptions: data.siblingOptions,n siblingSelection: data.siblingSelection,n currentPetCount: data.currentPetCount,n description: data.descriptionn });n });n }n handleFormSubmit() {n // 提交邏輯寫在這n }n handleClearForm() {n // 清除表單邏輯寫在這n }n render() {n return (n <form className="container" onSubmit={this.handleFormSubmit}>n <h5>Pet Adoption Form</h5>n <SingleInput /> {/* Full name text input */}n <Select /> {/* Owner age range select */}n <CheckboxOrRadioGroup /> {/* Pet type checkboxes */}n <CheckboxOrRadioGroup /> {/* Will you adopt siblings? radios */}n <SingleInput /> {/* Number of current pets number input */}n <TextArea /> {/* Descriptions of current pets textarea */}n <inputn type="submit"n className="btn btn-primary float-right"n value="Submit"/>n <buttonn className="btn btn-link float-left"n onClick={this.handleClearForm}>Clear form</button>n </form>n );n}n

我們勾勒出了應用基礎結構,接下來我們一起瀏覽下每個子組件的細節。

<SingleInput /> 組件

該組件可以是 text 或 number 輸入框,這取決於傳入的 props。通過 React 的 PropTypes,我們可以非常好地記錄組件拿到的 props。如果漏傳 props 或傳入錯誤的數據類型, 瀏覽器的控制台中會出現警告信息。

下面列舉 <SingleInput /> 組件的 PropTypes:

SingleInput.propTypes = { n inputType: React.PropTypes.oneOf([text, number]).isRequired,n title: React.PropTypes.string.isRequired,n name: React.PropTypes.string.isRequired,n controlFunc: React.PropTypes.func.isRequired,n content: React.PropTypes.oneOfType([n React.PropTypes.string,n React.PropTypes.number,n ]).isRequired,n placeholder: React.PropTypes.string,n};n

PropTypes 聲明了 prop 的類型(string、 number、 array、 object 等等),其中包括了必需(isRequired)和非必需的 prop,當然它還有更多的用途(欲知更多細節,請查看 React 文檔)。

下面我們逐個討論這些 PropType:

  1. inputType:接收兩個字元串:text 或 number。該設置指定渲染 <input type="text" /> 組件或 <input type="number" /> 組件。
  2. title:接收一個字元串,我們將它渲染到輸入框的 label 元素中。
  3. name:輸入框的 name 屬性。
  4. controlFunc:它是從父組件或容器組件傳下來的方法。因為該方法掛載在 React 的 onChange 處理方法上,所以每當輸入框的輸入值改變時,該方法都會被執行,從而更新父組件或容器組件的 state。
  5. content:輸入框內容。受控輸入框只會顯示通過 props 傳入的數據。
  6. placeholder:輸入框的佔位符文本,是一個字元串。

既然該組件不需要任何邏輯行為和內部 state,那我們可以將它寫成純函數組件(pure functional component)。我們將純函數組件賦值給一個 const 常量上。下面是 <SingleInput /> 組件的所有代碼。本文列舉的所有表單元素組件都是純函數組件。

import React from react;nnconst SingleInput = (props) => ( n <div className="form-group">n <label className="form-label">{props.title}</label>n <inputn className="form-input"n name={props.name}n type={props.inputType}n value={props.content}n onChange={props.controlFunc}n placeholder={props.placeholder} />n </div>n);nnSingleInput.propTypes = { n inputType: React.PropTypes.oneOf([text, number]).isRequired,n title: React.PropTypes.string.isRequired,n name: React.PropTypes.string.isRequired,n controlFunc: React.PropTypes.func.isRequired,n content: React.PropTypes.oneOfType([n React.PropTypes.string,n React.PropTypes.number,n ]).isRequired,n placeholder: React.PropTypes.string,n};nnexport default SingleInput; n

接著,我們用 handleFullNameChange 方法(它被傳入到 controlFunc prop 屬性)來更新 <FormContainer /> 容器組件的 state。

// FormContainer.jsnnhandleFullNameChange(e) { n this.setState({ ownerName: e.target.value });n}n// constructor 方法里別漏掉了這行:n// this.handleFullNameChange = this.handleFullNameChange.bind(this);n

隨後我們將容器組件更新後的 state (譯註:這裡指 state 上掛載的 ownerName 屬性)通過 content prop 傳回 <SingleInput /> 組件。

<Select /> 組件

選擇組件(就是下拉選擇組件),接收以下 props:

Select.propTypes = { n name: React.PropTypes.string.isRequired,n options: React.PropTypes.array.isRequired,n selectedOption: React.PropTypes.string,n controlFunc: React.PropTypes.func.isRequired,n placeholder: React.PropTypes.stringn};n

  1. name:填充表單元素上 name 屬性的字元串變數。
  2. options:是一個數組(本例是字元串數組)。通過在組件的 render 方法中使用 props.options.map(), 該數組中的每一項都會被渲染成一個選擇項。
  3. selectedOption:用以顯示錶單填充的默認選項,或用戶已選擇的選項(例如當用戶編輯之前已提交過的表單數據時,可以使用這個 prop)。
  4. controlFunc:它是從父組件或容器組件傳下來的方法。因為該方法掛載在 React 的 onChange 處理方法上,所以每當改變選擇框組件的值時,該方法都會被執行,從而更新父組件或容器組件的 state。
  5. placeholder:作為佔位文本的字元串,用來填充第一個 <option> 標籤。本組件中,我們將第一個選項的值設置成空字元串(參看下面代碼的第 10 行)。

import React from react;nnconst Select = (props) => ( n <div className="form-group">n <selectn name={props.name}n value={props.selectedOption}n onChange={props.controlFunc}n className="form-select">n <option value="">{props.placeholder}</option>n {props.options.map(opt => {n return (n <optionn key={opt}n value={opt}>{opt}</option>n );n })}n </select>n </div>n);nnSelect.propTypes = { n name: React.PropTypes.string.isRequired,n options: React.PropTypes.array.isRequired,n selectedOption: React.PropTypes.string,n controlFunc: React.PropTypes.func.isRequired,n placeholder: React.PropTypes.stringn};nnexport default Select; n

請注意 option 標籤中的 key 屬性(第 14 行)。React 要求被重複操作渲染的每個元素必須擁有獨一無二的 key 值,我們這裡的 .map() 方法就是所謂的重複操作。既然選擇項數組中的每個元素是獨有的,我們就把它們當成 key prop。該 key 值協助 React 追蹤 DOM 變化。雖然在循環操作或 mapping 時忘加 key 屬性不會中斷應用,但是瀏覽器的控制台里會出現警告,並且渲染性能將受到影響。

以下是控制選擇框組件(記住,該組件存在於 <FormContainer /> 組件中)的處理方法(該方法從 <FormContainer /> 組件傳入到子組件的 controlFun prop 中)

// FormContainer.jsnnhandleAgeRangeSelect(e) { n this.setState({ ownerAgeRangeSelection: e.target.value });n}n// constructor 方法里別漏掉了這行:n// this.handleAgeRangeSelect = this.handleAgeRangeSelect.bind(this);n

<CheckboxOrRadioGroup /> 組件

<CheckboxOrRadioGroup /> 與眾不同, 它從 props 拿到傳入的數組(像此前 <Select /> 組件的選項數組一樣),通過遍曆數組來渲染一組表單元素的集合 —— 可以是複選框集合或單選框集合。

讓我們深入 PropTypes 來更好地理解 <CheckboxOrRadioGroup /> 組件。

CheckboxGroup.propTypes = { n title: React.PropTypes.string.isRequired,n type: React.PropTypes.oneOf([checkbox, radio]).isRequired,n setName: React.PropTypes.string.isRequired,n options: React.PropTypes.array.isRequired,n selectedOptions: React.PropTypes.array,n controlFunc: React.PropTypes.func.isRequiredn};n

  1. title:一個字元串,用以填充單選或複選框集合的 label 標籤內容。
  2. type:接收 checkbox 或 radio 兩種配置的一種,並用指定的配置渲染輸入框(譯註:這裡指複選輸入框或單選輸入框)。
  3. setName:一個字元串,用以填充每個單選或複選框的 name 屬性值。
  4. options:一個由字元串元素組成的數組,數組元素用以渲染每個單選框或複選框的值和 label 的內容。例如,[dog, cat, pony] 數組中的元素將會渲染三個單選框或複選框。
  5. selectedOptions:一個由字元串元素組成的數組,用來表示預選項。在示例 4 中,如果 selectedOptions 數組包含 dog 和 pony 元素,那麼相應的兩個選項會被渲染成選中狀態,而 cat 選項則被渲染成未選中狀態。當用戶提交表單時,該數組將會是用戶的選擇數據。
  6. controlFunc:一個方法,用來處理從 selectedOptions 數組 prop 中添加或刪除字元串的操作。

這是本表單應用中最有趣的組件,讓我們來看一下:

import React from react;nnconst CheckboxOrRadioGroup = (props) => ( n <div>n <label className="form-label">{props.title}</label>n <div className="checkbox-group">n {props.options.map(opt => {n return (n <label key={opt} className="form-label capitalize">n <inputn className="form-checkbox"n name={props.setName}n onChange={props.controlFunc}n value={opt}n checked={ props.selectedOptions.indexOf(opt) > -1 }n type={props.type} /> {opt}n </label>n );n })}n </div>n </div>n);nnCheckboxOrRadioGroup.propTypes = { n title: React.PropTypes.string.isRequired,n type: React.PropTypes.oneOf([checkbox, radio]).isRequired,n setName: React.PropTypes.string.isRequired,n options: React.PropTypes.array.isRequired,n selectedOptions: React.PropTypes.array,n controlFunc: React.PropTypes.func.isRequiredn};nnexport default CheckboxOrRadioGroup; n

checked={ props.selectedOptions.indexOf(option) > -1 } 這一行代碼表示單選框或複選框是否被選中的邏輯。

屬性 checked 接收一個布爾值,用來表示 input 組件是否應該被渲染成選中狀態。我們在檢查到 input 的值是否是 props.selectedOptions 數組的元素之一時生成該布爾值。

myArray.indexOf(item) 方法返回 item 在數組中的索引值。如果 item 不在數組中,返回 -1,因此,我們寫了 > -1。

注意,0 是一個合法的索引值,所以我們需要 > -1 ,否則代碼會有 bug。如果沒有 > -1,selectedOptions 數組中的第一個 item —— 其索引為 0 —— 將永遠不會被渲染成選中狀態,因為 0 是一個類 false 的值(譯註:在 checked 屬性中,0 會被當成 false 處理)。

本組件的處理方法同樣比其他的有趣。

handlePetSelection(e) {nn const newSelection = e.target.value;n let newSelectionArray;nn if(this.state.selectedPets.indexOf(newSelection) > -1) {n newSelectionArray = this.state.selectedPets.filter(s => s !== newSelection)n } else {n newSelectionArray = [...this.state.selectedPets, newSelection];n }nn this.setState({ selectedPets: newSelectionArray });n}n

如同所有處理方法一樣,事件對象被傳入方法,這樣一來我們就能拿到事件對象的值(譯註:準確來說,應該是事件目標元素的值)。我們將該值賦給newSelection 常量。接著我們在函數頂部附近定義 newSelectionArray 變數。因為我們將在一個 if/else 代碼塊里對該變數進行賦值,所以用 let 而非 const 來定義它。我們在代碼塊外部進行定義,這樣一來被定義變數的作用域就是函數內部的最外沿,並且函數內的代碼塊都能訪問到外部定義的變數。

該方法需要處理兩種可能的情況。

如果 input 組件的值不在 selectedOptions 數組中,我們要將值添加進該數組。

如果 input 組件的值 selectedOptions 數組中,我們要從數組中刪除該值。

添加(第 8 - 10 行): 為了將新值添加進選項數組,我們通過解構舊數組(數組前的三點...表示解構)創建一個新數組,並且將新值添加到數組的尾部 newSelectionArray = [...this.state.selectedPets, newSelection];。

注意,我們創建了一個新數組,而不是通過類似 .push() 的方法來改變原數組。不改變已存在的對象和數組,而是創建新的對象和數組,這在 React 中是又一個最佳實踐。開發者這樣做可以更容易地跟蹤 state 的變化,而第三方 state 管理庫,如 Redux 則可以做高性能的淺比較,而不是阻塞性能的深比較。

刪除(第 6 - 8 行):if 代碼塊藉助此前用到的 .indexOf() 小技巧,檢查選項是否在數組中。如果選項已經在數組中,通過.filter()方法,該選項將被移除。 該方法返回一個包含所有滿足 filter 條件的元素的新數組(記住要避免在 React 直接修改數組或對象!)。

newSelectionArray = this.state.selectedPets.filter(s => s !== newSelection)n

在這種情況下,除了傳入到方法中的選項之外,其他選項都會被返回。

<TextArea /> 組件

<TextArea /> 和我們已提到的那些組件非常相似,除了 resize 和 rows,目前你應該對它的 props 很熟悉了。

TextArea.propTypes = { n title: React.PropTypes.string.isRequired,n rows: React.PropTypes.number.isRequired,n name: React.PropTypes.string.isRequired,n content: React.PropTypes.string.isRequired,n resize: React.PropTypes.bool,n placeholder: React.PropTypes.string,n controlFunc: React.PropTypes.func.isRequiredn};n

  1. title:接收一個字元串,用以渲染文本域的 label 標籤內容。
  2. rows:接收一個整數,用來指定文本域的行數。
  3. name:文本域的 name 屬性。
  4. content:文本域的內容。受控組件只會顯示通過 props 傳入的數據。
  5. resize: 接受一個布爾值,用來指定文本域能否調整大小。
  6. placeholder:充當文本域佔位文本的字元串。
  7. controlFunc: 它是從父組件或容器組件傳下來的方法。因為該方法掛載在 React 的 onChange 處理方法上,所以每當改變選擇框組件的值時,該方法都會被執行,從而更新父組件或容器組件的 state。

<TextArea /> 組件的完整代碼:

import React from react;nnconst TextArea = (props) => ( n <div className="form-group">n <label className="form-label">{props.title}</label>n <textarean className="form-input"n style={props.resize ? null : {resize: none}}n name={props.name}n rows={props.rows}n value={props.content}n onChange={props.controlFunc}n placeholder={props.placeholder} />n </div>n);nnTextArea.propTypes = { n title: React.PropTypes.string.isRequired,n rows: React.PropTypes.number.isRequired,n name: React.PropTypes.string.isRequired,n content: React.PropTypes.string.isRequired,n resize: React.PropTypes.bool,n placeholder: React.PropTypes.string,n controlFunc: React.PropTypes.func.isRequiredn};nnexport default TextArea; n

<TextAreas /> 組件的控制方法和 <SingleInput /> 如出一轍。細節部分請參考 <SingleInput /> 組件。

表單操作

handleClearForm 和 handleFormSubmit 方法操作整個表單。

1. handleClearForm

既然我們在表單的各處都使用了單向數據流,那麼清除表單數據對我們來說也是小菜一碟。<FormContainer /> 組件的 state 控制了每個表單元素的值。該容器的 state 通過 props 傳入子組件。只有當 <FormContainer /> 組件的 state 改變時,表單組件顯示的值才會改變。

清除表單子組件中顯示的數據很簡單,只要把容器的 state (譯註:這裡是指 state 對象上掛載的各個變數)設置成空數組和空字元串就可以了(如果有數字輸入框的話則是將值設置成 0)。

handleClearForm(e) { n e.preventDefault();n this.setState({n ownerName: ,n selectedPets: [],n ownerAgeRangeSelection: ,n siblingSelection: [],n currentPetCount: 0,n description: n });n}n

注意,e.preventDefault() 阻止了頁面重新載入,接著 setState() 方法用來清除表單數據。

2. handleFormSubmit

為了提交表單數據,我們從 state 中抽取需要提交的屬性值,創建了一個對象。接著使用 AJAX 庫或技術將這些數據發送給 API(本文不包含此類內容)。

handleFormSubmit(e) { n e.preventDefault();nn const formPayload = {n ownerName: this.state.ownerName,n selectedPets: this.state.selectedPets,n ownerAgeRangeSelection: this.state.ownerAgeRangeSelection,n siblingSelection: this.state.siblingSelection,n currentPetCount: this.state.currentPetCount,n description: this.state.descriptionn };nn console.log(Send this in a POST request:, formPayload);n this.handleClearForm(e);n}n

請注意我們在提交數據後執行 this.handleClearForm(e) 清除了表單。

表單校驗

受控表單組件非常適合自定義表單校驗。假設要從 <TextArea /> 組件中排除字母 "e",可以這樣做:

handleDescriptionChange(e) { n const textArray = e.target.value.split().filter(x => x !== e);nn console.log(string split into array of letters,textArray);nn const filteredText = textArray.join();n this.setState({ description: filteredText });n}n

把 e.target.value 字元串分割成字母數組,就生成了上述的 textArray。這樣字母 「e」 (或其他設法排除的字母)就被過濾掉了。再把剩餘的字母組成的數組拼成字元串,最後用該新字元串去設置組件 state。還不錯吧?

以上代碼放在本文的倉庫中,但我將它們注釋掉了,你可以按自己的需求自由地調整。

<FormContainer /> 組件

下面是我承諾給你們的 <FormContainer /> 組件完整代碼,

import React, {Component} from react; nimport CheckboxOrRadioGroup from ../components/CheckboxOrRadioGroup; nimport SingleInput from ../components/SingleInput; nimport TextArea from ../components/TextArea; nimport Select from ../components/Select;nnclass FormContainer extends Component { n constructor(props) {n super(props);n this.state = {n ownerName: ,n petSelections: [],n selectedPets: [],n ageOptions: [],n ownerAgeRangeSelection: ,n siblingOptions: [],n siblingSelection: [],n currentPetCount: 0,n description: n };n this.handleFormSubmit = this.handleFormSubmit.bind(this);n this.handleClearForm = this.handleClearForm.bind(this);n this.handleFullNameChange = this.handleFullNameChange.bind(this);n this.handleCurrentPetCountChange = this.handleCurrentPetCountChange.bind(this);n this.handleAgeRangeSelect = this.handleAgeRangeSelect.bind(this);n this.handlePetSelection = this.handlePetSelection.bind(this);n this.handleSiblingsSelection = this.handleSiblingsSelection.bind(this);n this.handleDescriptionChange = this.handleDescriptionChange.bind(this);n }n componentDidMount() {n // 模擬請求用戶數據n //(create-react-app 構建配置里包含了 fetch 的 polyfill)n fetch(./fake_db.json)n .then(res => res.json())n .then(data => {n this.setState({n ownerName: data.ownerName,n petSelections: data.petSelections,n selectedPets: data.selectedPets,n ageOptions: data.ageOptions,n ownerAgeRangeSelection: data.ownerAgeRangeSelection,n siblingOptions: data.siblingOptions,n siblingSelection: data.siblingSelection,n currentPetCount: data.currentPetCount,n description: data.descriptionn });n });n }n handleFullNameChange(e) {n this.setState({ ownerName: e.target.value });n }n handleCurrentPetCountChange(e) {n this.setState({ currentPetCount: e.target.value });n }n handleAgeRangeSelect(e) {n this.setState({ ownerAgeRangeSelection: e.target.value });n }n handlePetSelection(e) {n const newSelection = e.target.value;n let newSelectionArray;n if(this.state.selectedPets.indexOf(newSelection) > -1) {n newSelectionArray = this.state.selectedPets.filter(s => s !== newSelection)n } else {n newSelectionArray = [...this.state.selectedPets, newSelection];n }n this.setState({ selectedPets: newSelectionArray });n }n handleSiblingsSelection(e) {n this.setState({ siblingSelection: [e.target.value] });n }n handleDescriptionChange(e) {n this.setState({ description: e.target.value });n }n handleClearForm(e) {n e.preventDefault();n this.setState({n ownerName: ,n selectedPets: [],n ownerAgeRangeSelection: ,n siblingSelection: [],n currentPetCount: 0,n description: n });n }n handleFormSubmit(e) {n e.preventDefault();nn const formPayload = {n ownerName: this.state.ownerName,n selectedPets: this.state.selectedPets,n ownerAgeRangeSelection: this.state.ownerAgeRangeSelection,n siblingSelection: this.state.siblingSelection,n currentPetCount: this.state.currentPetCount,n description: this.state.descriptionn };nn console.log(Send this in a POST request:, formPayload)n this.handleClearForm(e);n }n render() {n return (n <form className="container" onSubmit={this.handleFormSubmit}>n <h5>Pet Adoption Form</h5>n <SingleInputn inputType={text}n title={Full name}n name={name}n controlFunc={this.handleFullNameChange}n content={this.state.ownerName}n placeholder={Type first and last name here} />n <Selectn name={ageRange}n placeholder={Choose your age range}n controlFunc={this.handleAgeRangeSelect}n options={this.state.ageOptions}n selectedOption={this.state.ownerAgeRangeSelection} />n <CheckboxOrRadioGroupn title={Which kinds of pets would you like to adopt?}n setName={pets}n type={checkbox}n controlFunc={this.handlePetSelection}n options={this.state.petSelections}n selectedOptions={this.state.selectedPets} />n <CheckboxOrRadioGroupn title={Are you willing to adopt more than one pet if we have siblings for adoption?}n setName={siblings}n controlFunc={this.handleSiblingsSelection}n type={radio}n options={this.state.siblingOptions}n selectedOptions={this.state.siblingSelection} />n <SingleInputn inputType={number}n title={How many pets do you currently own?}n name={currentPetCount}n controlFunc={this.handleCurrentPetCountChange}n content={this.state.currentPetCount}n placeholder={Enter number of current pets} />n <TextArean title={If you currently own pets, please write their names, breeds, and an outline of their personalities.}n rows={5}n resize={false}n content={this.state.description}n name={currentPetInfo}n controlFunc={this.handleDescriptionChange}n placeholder={Please be thorough in your descriptions} />n <inputn type="submit"n className="btn btn-primary float-right"n value="Submit"/>n <buttonn className="btn btn-link float-left"n onClick={this.handleClearForm}>Clear form</button>n </form>n );n }n}nnexport default FormContainer;n

總結

我承認用 React 構建受控表單組件要做一些重複勞動(比如容器組件中的處理方法),但就你對應用的掌控度和 state 變更的透明度來說,預先投入精力是超值的。你的代碼會變得可維護並且很高效。

如果想在我發布新文章時接到通知,你可以在博客的導航欄部分註冊我的郵件發送清單。

iKcamp原創新書《移動Web前端高效開發實戰》已在亞馬遜、京東、噹噹開售。

推薦閱讀:

SF 講堂推薦「寫 CSS 也要開腦洞:萬能的 `:checked + label`」要開播啦
前端學習幾年後遇到瓶頸了,該怎麼辦?

TAG:前端开发 | React | 前端组件 |