作者:霜序
校稿:袋鼠云數堆疊前端團隊運營小組
該文章包含如下內容
- 受控與非受控組件
- 非受控組件
- 受控組件
- 受控和非受控組件邊界
- 反模式
- 解決方案
前言
在 HTML 中,表單元素(<input>/<textarea>/<select>),通常自己會維護 state,并根據用戶的輸入進行更新
<form>
<label>
名字:
<input type="text" name="name" />
</label>
<input type="submit" value="https://www.cnblogs.com/dtux/archive/2022/03/17/提交" />
</form>
在這個 HTML 中,我們可以在 input 中隨意的輸入值,如果我們需要獲取到當前 input 所輸入的內容,應該怎么做呢?
受控與非受控組件
非受控組件(uncontrolled component)
使用非受控組件,不是為每個狀態更新撰寫資料處理函式,而是將表單資料交給 DOM 節點來處理,可以使用 Ref 來獲取資料
在非受控組件中,希望能夠賦予表單一個初始值,但是不去控制后續的更新,可以采用defaultValue指定一個默認值
class Form extends Component {
handleSubmitClick = () => {
const name = this._name.value;
// do something with `name`
}
render() {
return (
<div>
<input
type="text"
defaultValue="https://www.cnblogs.com/dtux/archive/2022/03/17/Bob"
ref={input => this._name = input}
/>
<button onClick={this.handleSubmitClick}>Sign up</button>
</div>
);
}
}
受控組件(controlled component)
在 React 中,可變狀態(mutable state)通常保存在組件的 state 屬性中,并且只能夠通過setState 來更新
class NameForm extends React.Component {
constructor(props) {
super(props);
this.state = {value: 'shuangxu'};
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
名字:
<input type="text" value=https://www.cnblogs.com/dtux/archive/2022/03/17/{this.state.value}/>
在上述的代碼中,在 Input 設定了 value 屬性值,因此顯示的值始終為this.state.value,這使得 state 成為了唯一的資料源,
const handleChange = (event) => {
this.setState({ value: event.target.value })
}
<input type="text" value=https://www.cnblogs.com/dtux/archive/2022/03/17/{this.state.value} onChange={this.handleChange}/>
如果我們在上面的示例中寫入handleChange 方法,那么每次按鍵都會執行該方法并且更新 React 的 state,因此表單的值將隨著用戶的輸入而改變
React 組件控制著用戶輸入程序中表單發生的操作并且 state 還是唯一資料源,被 React 以這種方式控制取值的表單輸入元素叫做受控組件
受控和非受控組件邊界
非受控組件
Input 組件只接收一個defaultValue默認值,呼叫 Input 組件的時候,只需要通過 props 傳遞一個defaultValue 即可
//組件
function Input({defaultValue}){
return <input defaultValue=https://www.cnblogs.com/dtux/archive/2022/03/17/{defaultValue} />
}
//呼叫
function Demo(){
return
受控組件
數值的展示和變更需要由state和setState,組件內部控制 state,并實作自己的 onChange 方法
//組件
function Input() {
const [value, setValue] = useState('shuangxu')
return <input value=https://www.cnblogs.com/dtux/archive/2022/03/17/{value} onChange={e=>setValue(e.target.value)} />;
}
//呼叫
function Demo() {
return ;
}
請問這時 Input 組件是受控還是非受控?如果我們采用之前的寫法更改這個組件以及其呼叫
//組件
function Input({defaultValue}) {
const [value, setValue] = useState(defaultValue)
return <input value=https://www.cnblogs.com/dtux/archive/2022/03/17/{value} onChange={e=>setValue(e.target.value)} />;
}
//呼叫
function Demo() {
return
此時的 Input 組件本身是一個受控組件,它是由唯一的 state 資料驅動的,但是對于 Demo 來說,我們并沒有 Input 組件的一個資料變更權利,那么對于 Demo 組件來說,Input 組件就是一個非受控組件,(??以非受控組件的方式去呼叫受控組件是一種反模式)
如何修改當前的 Input 和 Demo 組件代碼,才能夠使得 Input 組件本身也是一個受控組件,并且對于 Demo 組件來說它也是受控的訥?
function Input({value, onChange}){
return <input value=https://www.cnblogs.com/dtux/archive/2022/03/17/{value} onChange={onChange}
}
function Demo(){
const [value, setValue] = useState('shuangxu')
return <Input value=https://www.cnblogs.com/dtux/archive/2022/03/17/{value} onChange={e => setValue(e.target.value)} />
反模式-以非受控組件的方式去呼叫受控組件
雖然受控和非受控通常用來指向表單的 inputs,也能用來描述資料頻繁更新的組件,
通過上一節受控與非受控組件的邊界劃分,我們可以簡單的分類為:
- 如果使用 props 傳入資料,有對應的資料處理方法,組件對于父級來說認為是可控的
- 資料只是保存在組件內部的 state 中,組件對于父級來說是非受控的
??什么是派生 state
簡單來說,如果一個組件的 state 中的某個資料來自外部,就將該資料稱之為派生狀態,
大部分使用派生 state 導致的問題,不外乎兩個原因:
- 直接復制 props 到 state
- 如果 props 和 state 不一致就更新 state
直接復制 prop 到 state
??getDerivedStateFromProps和componentWillReceiveProps的執行時期
- 在父級重新渲染時,不管 props 是否有變化,這兩個生命周期都會執行
- 所以在兩個方法里面直接復制 props 到 state 是不安全的,會導致 state 沒有正確渲染
class EmailInput extends React.Component {
constructor(props) {
super(props);
this.state = {
email: this.props.email //初始值為props中email
};
}
componentWillReceiveProps(nextProps) {
this.setState({ email: nextProps.email }); //更新時,重新給state賦值
}
handleChange = (e) => {
this.setState({ email: e.target.value });
};
render() {
const { email } = this.state;
return <input value=https://www.cnblogs.com/dtux/archive/2022/03/17/{email} onChange={this.handleChange} />;
}
}
點擊查看示例
給 Input 設定 props 傳來的初始值,在 Input 輸入時它會修改 state,但是如果父組件重新渲染時,輸入框 Input 的值就會丟失,變成 props 的默認值
即使我們在重置前比較nextProps.email!==this.state.email仍然會導致更新
針對于目前這個小 demo 來說,可以使用shouldComponentUpdate來比較 props 中的 email 是否修改再來決定是否需要重新渲染,但是對于實際應用來說,這種處理方式并不可行,一個組件會接收多個 prop,任何一個 prop 的改變都會導致重新渲染和不正確的狀態重置,加上行內函式和物件 prop,創建一個完全可靠的shouldComponentUpdate會變得越來越難,shouldComponentUpdate這個生命周期更多是用于性能優化,而不是處理派生 state,
截止這里,講清為什么不能直接復制 prop 到 state,思考另一個問題,如果只使用 props 中的 email 屬性更新組件訥?
在 props 變化后修改 state
接著上述示例,只使用props.email來更新組件,這樣可以防止修改 state 導致的 bug
class EmailInput extends React.Component {
constructor(props) {
super(props);
this.state = {
email: this.props.email //初始值為props中email
};
}
componentWillReceiveProps(nextProps) {
if(nextProps.email !== this.props.email){
this.setState({ email: nextProps.email }); //email改變時,重新給state賦值
}
}
//...
}
通過這個改造,組件只有在 props.email 改變時才會重新給 state 賦值,那這樣改造會有問題嗎?
在下列場景中,對擁有兩個相同 email 的賬號進行切換的時,這個輸入框不會重置,因為父組件傳來的 prop 值沒有任何變化
點擊查看示例
這個場景是構建出來的,可能設計奇怪,但是這樣子的錯誤很常見,對于這種反模式來說,有兩種方案可以解決這些問題,關鍵在于,任何資料,都要保證只有一個資料來源,而且避免直接復制它,
解決方案
完全可控的組件
從 EmailInput 組件中洗掉 state,直接使用 props 來獲取值,將受控組件的控制權交給父組件,
function EmailInput(props){
return <input onChange={props.onChange} value=https://www.cnblogs.com/dtux/archive/2022/03/17/{props.email}/>
}
如果想要保存臨時的值,需要父組件手動執行保存,
有 key 的非受控組件
讓組件存盤臨時的 email state,email 的初始值仍然是通過 prop 來接受的,但是更改之后的值就和 prop 沒有關系了
function EmailInput(props){
const [email, setEmail] = useState(props.email)
return <input value=https://www.cnblogs.com/dtux/archive/2022/03/17/{email} onChange={(e) => setEmail(e.target.value)}/>
}
在之前的切換賬號的示例中,為了在不同頁面切換不同的值,可以使用key這個 React 特殊屬性,當 key 變化時,React 會創建一個新的組件而不是簡單的更新存在的組件(獲取更多),我們經常使用在渲染動態串列時使用 key 值,這里也可以使用,
<EmailInput
email={account.email}
key={account.id}
/>
點擊查看示例
每次 id 改變的時候,都會重新創建EmailInput,并將其狀態重置為最近 email 值,
可選方案
- 使用 key 屬性來做,會使組件整個組件的 state 都重置,可以在
getDerivedStateFromProps和componentWillReceiveProps來觀察 id 的變化,麻煩但是可行
點擊查看示例
class EmailInput extends Component {
state = {
email: this.props.email,
prevId: this.props.id
};
componentWillReceiveProps(nextProps) {
const { prevId } = this.state;
if (nextProps.id !== prevId) {
this.setState({
email: nextProps.email,
prevId: nextProps.id
});
}
}
// ...
}
- 使用實體方法重置非受控組件
剛剛兩種方式,均是再有唯一標識值的情況下,如果在沒有合適的key值時,也想要重新創建組件,第一種方案就是生成隨機值或者遞增的值當作key值,另一種就是使用示例方法強制重置內部狀態
父組件使用ref呼叫這個方法,點擊查看示例
class EmailInput extends Component {
state = {
email: this.props.email
};
resetEmailForNewUser(newEmail) {
this.setState({ email: newEmail });
}
// ...
}
那我們如何選
在我們的業務開發中,盡量選擇受控組件,減少使用派生 state,過量的使用 componentWillReceiveProps 可能導致 props 判斷不夠完善,倒是重復渲染死回圈問題,
在組件庫開發中,例如 Ant Design,將受控與非受控的呼叫方式都開放給用戶,讓用戶自主選擇對應的呼叫方式,比如 Form 組件,我們常使用 getFieldDecorator 和 initialValue 來定義表單項,但是我們根本不關心中間的輸入程序,在最后提交的時候通過 getFieldsValue 或者 validateFields 拿到所有的表單值,這就是非受控的呼叫方式,或者是,我們在只有一個 Input 的時候,我們可以直接系結 value 和 onChange 事件,這也就是受控的方式呼叫,
總結
在本文中,首先介紹了非受控組件和受控組件的概念,對于受控組件來說,組件控制用戶輸入的程序以及 state 是受控組件唯一的資料來源,
接著介紹了組件的呼叫問題,對于組件呼叫方而言,組件提供方是否為受控組件,對于呼叫方而言,組件受控以及非受控的邊界劃分取決于當前組件對于子組件值的變更是否擁有控制權,
接著介紹了以非受控組件的方式呼叫受控組件這種反模式用法,以及相關示例,不要直接復制 props 到 state,而是使用受控組件,對于不受控的組件,當你想在 prop 變化時重置 state 的話,可以選擇以下幾種方式:
- 建議: 使用
key屬性,重置內部所有的初始 state - 選項一:僅更改某些欄位,觀察特殊屬性的變化(具有唯一性的屬性)
- 選項二:使用 ref 呼叫實體方法
最后總結了一下,應當如何選擇受控組件還是非受控組件,
參考鏈接
- React 官網——受控組件
- React 官網——非受控組件
- controlled vs. uncontrolled form inputs
- Transitioning from uncontrolled inputs to controlled
- 重新認識受控非受控組件
- 你可能不需要使用派生 state
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/445552.html
標籤:其他
下一篇:【軟體】重構與架構
