在上一篇小甜點 《我們或許不需要 classnames 這個庫》 中, 我們 簡單的使用了一些語法代替了 classnames 這個庫
現在我們調整一下難度, 移除 React 中相對比較複雜的元件: Form 元件
在移除 Form 元件之前, 我們現需要進行一些思考, 為什麼會有 Form 元件及Form元件和 React 狀態管理的關係
注意, 接下來的內容非常容易讓 React 開發人員感到不適, 並且極具爭議性
何時不應該使用受控元件
Angular, Vue, 都有雙向繫結, 而 React 官方文件也為一個 input 標籤的雙向繫結給了一個官方方案 - 受控元件:
本文中提到的程式碼都可以直接貼上至專案中進行驗證.
// 以下是官方的受控元件例子:
class NameForm extends React.Component {
constructor(props) {
super(props);
this.state = {value: ''};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(event) {
this.setState({value: event.target.value});
}
handleSubmit(event) {
alert('A name was submitted: ' + this.state.value);
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Name:
<input type="text" value={this.state.value} onChange={this.handleChange} />
</label>
<input type="submit" value="Submit" />
</form>
);
}
}
複製程式碼
相信寫過 React 專案的人都已經非常熟練, 受控元件就是: 把一個 input 的 value 和 onChange 關聯到某一個狀態中.
很長一段時間, 使用受控元件, 我們都會受到以下幾個困惑:
- 針對較多表單內容的頁面, 編寫受控元件繁瑣
- 跨元件的受控元件需要使用 onChange 等 props 擊鼓傳花, 層層傳遞, 這種情況下做表單聯動就會變得麻煩
社群對以上的解決方案是提供一些表單元件, 比較常用的有:
- Ant Design Form 元件
- no-form 元件
- react-final-form 元件(有hooks版本)
包括我自己也編寫過 Form 元件
它們解決了以下幾個問題:
- 跨元件獲取表單內容
- 表單聯動
- 根據條件去執行或修改表單元件的某些行為, 如:
- 表單校驗
- props屬性控制
- ref獲取函式並執行
其實這些表單都是基於 React 官方受控元件的封裝, 其中 Antd Form 及 no-form 都是參考我們的先知
Dan Abramov 的理念:
單向資料流, 狀態管理至頂而下; 這樣可以確保整個架構資料的同步, 加強專案的穩定性; 它滿足以下 4 個特點:
- 不阻斷資料流
- 時刻準備渲染
- 沒有單例元件
- 隔離本地狀態
Dan Abramov 具體的文章在此處: 編寫有彈性的元件
我一直極力推崇單向資料流的方案, 在之前的專案中一直以 redux + immutable 作為專案管理, 專案也一直穩定執行, 直到 React-Hooks 的方案出現(這是另外的話題).
單向資料流的特點是用計算時間換開發人員的時間, 我們舉一個小例子說明:
如果當前元件樹中有 100 個 元件, 其中50個元件被connect注入了狀態, 那麼當發起一個 dispatch 行為, 需要更新1個元件, 這50個元件會被更新, 我們需要使用 immutable 在 shouldComponentUpdate 中進行高效的判斷, 以攔截另外49個不必要更新的元件.
單向資料流的好處是我們永遠只需要維護最頂部的狀態, 減少了系統的混亂程度.
缺點也是明顯的: 我們需要額外的判斷是否更新的開銷
大部分 Form 表單獲取資料的思路也是一個內聚的單向資料流, 每次 onChange 就修改 Form 中的 state, 子元件通過註冊 context, 獲取及更新相應的值. 這是滿足 Dan Abramov 的設計理念的.
而 react-final-form 沒有使用以上模式, 而是通過釋出訂閱, 把每個元件的更新加入訂閱, 根據行為進行相應的更新, 按照以上的例子, 它們是如此運作:
如果當前元件樹中有 100 個 元件, 其中50個元件被Form標記了, 那麼當發起一個 input 行為, 需要更新1個元件, 會找到這一個元件, 在內部進行setState, 並把相應的值更新到 Form 中的 data 中.
這種設計有沒有違背 React 的初衷呢? 我認為是沒有的, 因為 Form 維護的內容是區域性的, 而不是整體的, 我們只需要讓整個 Form 不脫離資料流的管理即可.
通過 react-final-form 這個元件的例子我想明白了一件事情:
-
單向資料流是幫我們更容易的管理, 但是並不是表示非單向資料流狀態就一定混亂, 就如 react-final-form 元件所管理的表單狀態.
-
既然 react-final-form 可以這麼設計, 我們為什麼不能設計區域性的, 脫離受控元件的範疇的表單?
好的, 可以進入正題了:
表單內部的元件可以脫離受控元件存在, 只需要讓表單本身為受控元件
使用 form 標籤代替 React Form 元件
我們用一個簡單的例子實現最開始React官方的受控元件的示例程式碼:
class App extends React.Component {
formDatas = {};
handleOnChange = event => {
// 在input事件中, 我們將dom元素的值儲存起來, 用於表單提交
this.formDatas[event.target.name] = event.target.value;
};
handleOnSubmit = event => {
console.log('formDatas: ', this.formDatas);
event.preventDefault();
};
render() {
return (
<form onChange={this.handleOnChange} onSubmit={this.handleOnSubmit}>
<input name="username" />
<input name="password" />
<button type="submit" />
</form>
);
}
}
複製程式碼
這是最簡單的獲取值, 儲存到一個物件中, 我們會一步步描述如何脫離受控元件進行值和狀態管理, 但是為了後續的程式碼更加簡潔, 我們使用 hooks 完成以上行為:
獲取表單內容
function App() {
// 使用 useRef 來儲存資料, 這樣可以防止函式每次被重新執行時無法儲存變數
const { current: formDatas } = React.useRef({});
// 使用 useCallback 來宣告函式, 減少元件重繪時重新宣告函式的開銷
const handleOnChange = React.useCallback(event => {
// 在input事件中, 我們將dom元素的值儲存起來, 用於表單提交
formDatas[event.target.name] = event.target.value;
}, []);
const handleOnSubmit = React.useCallback(event => {
// 提交表單
console.log('formDatas: ', formDatas);
event.preventDefault();
}, []);
return (
<form onChange={handleOnChange} onSubmit={handleOnSubmit}>
<input name="username" />
<input name="password" />
<button type="submit" />
</form>
);
}
複製程式碼
接下來的程式碼都會在此基礎上, 使用 hooks 語法編寫
跨元件獲取表單內容
我們不需要做任何處理,