重新認(rèn)識(shí)受控和非受控組件

作者:霜序

校稿:袋鼠云數(shù)棧前端團(tuán)隊(duì)運(yùn)營(yíng)小組

該文章包含如下內(nèi)容

  • 受控與非受控組件
    • 非受控組件
    • 受控組件
  • 受控和非受控組件邊界
  • 反模式
  • 解決方案

前言

在 HTML 中摆出,表單元素(<input>/<textarea>/<select>),通常自己會(huì)維護(hù) state,并根據(jù)用戶的輸入進(jìn)行更新

<form>
  <label>
    名字:
    <input type="text" name="name" />
  </label>
  <input type="submit" value="提交" />
</form>

在這個(gè) HTML 中志衣,我們可以在 input 中隨意的輸入值窿吩,如果我們需要獲取到當(dāng)前 input 所輸入的內(nèi)容或链,應(yīng)該怎么做呢秽誊?

受控與非受控組件

非受控組件(uncontrolled component)

使用非受控組件鲸沮,不是為每個(gè)狀態(tài)更新編寫數(shù)據(jù)處理函數(shù),而是將表單數(shù)據(jù)交給 DOM 節(jié)點(diǎn)來處理养距,可以使用 Ref 來獲取數(shù)據(jù)
在非受控組件中,希望能夠賦予表單一個(gè)初始值日熬,但是不去控制后續(xù)的更新棍厌。可以采用defaultValue指定一個(gè)默認(rèn)值

class Form extends Component {
  handleSubmitClick = () => {
    const name = this._name.value;
    // do something with `name`
  }
  render() {
    return (
      <div>
        <input
                    type="text"
                    defaultValue="Bob"
                    ref={input => this._name = input}
                />
        <button onClick={this.handleSubmitClick}>Sign up</button>
      </div>
    );
  }
}

受控組件(controlled component)

在 React 中竖席,可變狀態(tài)(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={this.state.value}/>
        </label>
        <input type="submit" value="提交" />
      </form>
    );
  }
}

在上述的代碼中,在 Input 設(shè)置了 value 屬性值毕荐,因此顯示的值始終為this.state.value束析,這使得 state 成為了唯一的數(shù)據(jù)源。

const handleChange = (event) => {
    this.setState({ value: event.target.value })
}

<input type="text" value={this.state.value} onChange={this.handleChange}/>

如果我們?cè)谏厦娴氖纠袑懭?code>handleChange 方法憎亚,那么每次按鍵都會(huì)執(zhí)行該方法并且更新 React 的 state,因此表單的值將隨著用戶的輸入而改變

React 組件控制著用戶輸入過程中表單發(fā)生的操作并且 state 還是唯一數(shù)據(jù)源员寇,被 React 以這種方式控制取值的表單輸入元素叫做受控組件

受控和非受控組件邊界

非受控組件

Input 組件只接收一個(gè)defaultValue默認(rèn)值,調(diào)用 Input 組件的時(shí)候第美,只需要通過 props 傳遞一個(gè)defaultValue 即可

//組件
function Input({defaultValue}){
    return <input defaultValue={defaultValue} />  
}

//調(diào)用
function Demo(){
    return <Input defaultValue='shuangxu' />
}

受控組件

數(shù)值的展示和變更需要由statesetState蝶锋,組件內(nèi)部控制 state,并實(shí)現(xiàn)自己的 onChange 方法

//組件
function Input() {
    const [value, setValue] = useState('shuangxu')
  return <input value={value} onChange={e=>setValue(e.target.value)} />;
}

//調(diào)用
function Demo() {
  return <Input />;
}

請(qǐng)問這時(shí) Input 組件是受控還是非受控什往?如果我們采用之前的寫法更改這個(gè)組件以及其調(diào)用

//組件
function Input({defaultValue}) {
    const [value, setValue] = useState(defaultValue)
  return <input value={value} onChange={e=>setValue(e.target.value)} />;
}

//調(diào)用
function Demo() {
  return <Input defaultValue='shuangxu' />;
}

此時(shí)的 Input 組件本身是一個(gè)受控組件扳缕,它是由唯一的 state 數(shù)據(jù)驅(qū)動(dòng)的。但是對(duì)于 Demo 來說别威,我們并沒有 Input 組件的一個(gè)數(shù)據(jù)變更權(quán)利躯舔,那么對(duì)于 Demo 組件來說,Input 組件就是一個(gè)非受控組件省古。(??以非受控組件的方式去調(diào)用受控組件是一種反模式)

如何修改當(dāng)前的 Input 和 Demo 組件代碼粥庄,才能夠使得 Input 組件本身也是一個(gè)受控組件,并且對(duì)于 Demo 組件來說它也是受控的訥豺妓?

function Input({value, onChange}){
    return <input value={value} onChange={onChange}
}

function Demo(){
    const [value, setValue] = useState('shuangxu')
    return <Input value={value} onChange={e => setValue(e.target.value)} />

反模式-以非受控組件的方式去調(diào)用受控組件

雖然受控和非受控通常用來指向表單的 inputs飒赃,也能用來描述數(shù)據(jù)頻繁更新的組件利花。
通過上一節(jié)受控與非受控組件的邊界劃分,我們可以簡(jiǎn)單的分類為:

  • 如果使用 props 傳入數(shù)據(jù)载佳,有對(duì)應(yīng)的數(shù)據(jù)處理方法炒事,組件對(duì)于父級(jí)來說認(rèn)為是可控的
  • 數(shù)據(jù)只是保存在組件內(nèi)部的 state 中,組件對(duì)于父級(jí)來說是非受控的

??什么是派生 state

簡(jiǎn)單來說蔫慧,如果一個(gè)組件的 state 中的某個(gè)數(shù)據(jù)來自外部挠乳,就將該數(shù)據(jù)稱之為派生狀態(tài)。

大部分使用派生 state 導(dǎo)致的問題姑躲,不外乎兩個(gè)原因:

  • 直接復(fù)制 props 到 state
  • 如果 props 和 state 不一致就更新 state

直接復(fù)制 prop 到 state

??getDerivedStateFromPropscomponentWillReceiveProps的執(zhí)行時(shí)期

  • 在父級(jí)重新渲染時(shí)睡扬,不管 props 是否有變化,這兩個(gè)生命周期都會(huì)執(zhí)行
  • 所以在兩個(gè)方法里面直接復(fù)制 props 到 state 是不安全的黍析,會(huì)導(dǎo)致 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 });   //更新時(shí)卖怜,重新給state賦值
  }
  handleChange = (e) => {
    this.setState({ email: e.target.value });
  };
  render() {
    const { email } = this.state;
    return <input value={email} onChange={this.handleChange} />;
  }
}

點(diǎn)擊查看示例

給 Input 設(shè)置 props 傳來的初始值,在 Input 輸入時(shí)它會(huì)修改 state阐枣。但是如果父組件重新渲染時(shí)马靠,輸入框 Input 的值就會(huì)丟失,變成 props 的默認(rèn)值

即使我們?cè)谥刂们氨容^nextProps.email!==this.state.email仍然會(huì)導(dǎo)致更新

針對(duì)于目前這個(gè)小 demo 來說蔼两,可以使用shouldComponentUpdate來比較 props 中的 email 是否修改再來決定是否需要重新渲染甩鳄。但是對(duì)于實(shí)際應(yīng)用來說,這種處理方式并不可行额划,一個(gè)組件會(huì)接收多個(gè) prop妙啃,任何一個(gè) prop 的改變都會(huì)導(dǎo)致重新渲染和不正確的狀態(tài)重置。加上行內(nèi)函數(shù)和對(duì)象 prop俊戳,創(chuàng)建一個(gè)完全可靠的shouldComponentUpdate會(huì)變得越來越難揖赴。shouldComponentUpdate這個(gè)生命周期更多是用于性能優(yōu)化,而不是處理派生 state抑胎。
截止這里储笑,講清為什么不能直接復(fù)制 prop 到 state。思考另一個(gè)問題圆恤,如果只使用 props 中的 email 屬性更新組件訥突倍?

在 props 變化后修改 state

接著上述示例,只使用props.email來更新組件盆昙,這樣可以防止修改 state 導(dǎo)致的 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改變時(shí)羽历,重新給state賦值
        }
  }
    //...
}

通過這個(gè)改造,組件只有在 props.email 改變時(shí)才會(huì)重新給 state 賦值淡喜,那這樣改造會(huì)有問題嗎秕磷?

在下列場(chǎng)景中,對(duì)擁有兩個(gè)相同 email 的賬號(hào)進(jìn)行切換的時(shí)炼团,這個(gè)輸入框不會(huì)重置澎嚣,因?yàn)楦附M件傳來的 prop 值沒有任何變化
點(diǎn)擊查看示例
這個(gè)場(chǎng)景是構(gòu)建出來的疏尿,可能設(shè)計(jì)奇怪,但是這樣子的錯(cuò)誤很常見易桃。對(duì)于這種反模式來說褥琐,有兩種方案可以解決這些問題。關(guān)鍵在于晤郑,任何數(shù)據(jù)敌呈,都要保證只有一個(gè)數(shù)據(jù)來源,而且避免直接復(fù)制它造寝。

解決方案

完全可控的組件

從 EmailInput 組件中刪除 state磕洪,直接使用 props 來獲取值,將受控組件的控制權(quán)交給父組件诫龙。

function EmailInput(props){
    return <input onChange={props.onChange} value={props.email}/>
}

如果想要保存臨時(shí)的值析显,需要父組件手動(dòng)執(zhí)行保存。

有 key 的非受控組件

讓組件存儲(chǔ)臨時(shí)的 email state签赃,email 的初始值仍然是通過 prop 來接受的谷异,但是更改之后的值就和 prop 沒有關(guān)系了

function EmailInput(props){
    const [email, setEmail] = useState(props.email)
    return <input value={email} onChange={(e) => setEmail(e.target.value)}/>
}

在之前的切換賬號(hào)的示例中,為了在不同頁面切換不同的值姊舵,可以使用key這個(gè) React 特殊屬性晰绎。當(dāng) key 變化時(shí)寓落,React 會(huì)創(chuàng)建一個(gè)新的組件而不是簡(jiǎn)單的更新存在的組件(獲取更多)括丁。我們經(jīng)常使用在渲染動(dòng)態(tài)列表時(shí)使用 key 值,這里也可以使用伶选。

<EmailInput
    email={account.email}
    key={account.id}
/>

點(diǎn)擊查看示例

每次 id 改變的時(shí)候史飞,都會(huì)重新創(chuàng)建EmailInput,并將其狀態(tài)重置為最近 email 值仰税。

可選方案

  1. 使用 key 屬性來做构资,會(huì)使組件整個(gè)組件的 state 都重置≡纱兀可以在getDerivedStateFromPropscomponentWillReceiveProps 來觀察 id 的變化吐绵,麻煩但是可行
    點(diǎn)擊查看示例
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
      });
    }
  }
  // ...
}
  1. 使用實(shí)例方法重置非受控組件
    剛剛兩種方式,均是再有唯一標(biāo)識(shí)值的情況下河绽。如果在沒有合適的key值時(shí)己单,也想要重新創(chuàng)建組件。第一種方案就是生成隨機(jī)值或者遞增的值當(dāng)作key值耙饰,另一種就是使用示例方法強(qiáng)制重置內(nèi)部狀態(tài)
    父組件使用ref調(diào)用這個(gè)方法纹笼,點(diǎn)擊查看示例
class EmailInput extends Component {
  state = {
    email: this.props.email
  };

  resetEmailForNewUser(newEmail) {
    this.setState({ email: newEmail });
  }

  // ...
}

那我們?nèi)绾芜x

在我們的業(yè)務(wù)開發(fā)中,盡量選擇受控組件苟跪,減少使用派生 state廷痘,過量的使用 componentWillReceiveProps 可能導(dǎo)致 props 判斷不夠完善蔓涧,倒是重復(fù)渲染死循環(huán)問題。

在組件庫開發(fā)中笋额,例如 Ant Design元暴,將受控與非受控的調(diào)用方式都開放給用戶,讓用戶自主選擇對(duì)應(yīng)的調(diào)用方式鳞陨。比如 Form 組件昨寞,我們常使用 getFieldDecorator 和 initialValue 來定義表單項(xiàng),但是我們根本不關(guān)心中間的輸入過程厦滤,在最后提交的時(shí)候通過 getFieldsValue 或者 validateFields 拿到所有的表單值援岩,這就是非受控的調(diào)用方式√偷迹或者是享怀,我們?cè)谥挥幸粋€(gè) Input 的時(shí)候,我們可以直接綁定 value 和 onChange 事件趟咆,這也就是受控的方式調(diào)用添瓷。

總結(jié)

在本文中,首先介紹了非受控組件和受控組件的概念值纱。對(duì)于受控組件來說鳞贷,組件控制用戶輸入的過程以及 state 是受控組件唯一的數(shù)據(jù)來源。

接著介紹了組件的調(diào)用問題虐唠,對(duì)于組件調(diào)用方而言搀愧,組件提供方是否為受控組件。對(duì)于調(diào)用方而言疆偿,組件受控以及非受控的邊界劃分取決于當(dāng)前組件對(duì)于子組件值的變更是否擁有控制權(quán)咱筛。

接著介紹了以非受控組件的方式調(diào)用受控組件這種反模式用法,以及相關(guān)示例杆故。不要直接復(fù)制 props 到 state迅箩,而是使用受控組件。對(duì)于不受控的組件处铛,當(dāng)你想在 prop 變化時(shí)重置 state 的話饲趋,可以選擇以下幾種方式:

  • 建議: 使用key屬性,重置內(nèi)部所有的初始 state
  • 選項(xiàng)一:僅更改某些字段撤蟆,觀察特殊屬性的變化(具有唯一性的屬性)
  • 選項(xiàng)二:使用 ref 調(diào)用實(shí)例方法

最后總結(jié)了一下奕塑,應(yīng)當(dāng)如何選擇受控組件還是非受控組件。

參考鏈接

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末枫疆,一起剝皮案震驚了整個(gè)濱河市爵川,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌息楔,老刑警劉巖寝贡,帶你破解...
    沈念sama閱讀 216,324評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件扒披,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡圃泡,警方通過查閱死者的電腦和手機(jī)碟案,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來颇蜡,“玉大人价说,你說我怎么就攤上這事》绯樱” “怎么了鳖目?”我有些...
    開封第一講書人閱讀 162,328評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)缤弦。 經(jīng)常有香客問我领迈,道長(zhǎng),這世上最難降的妖魔是什么碍沐? 我笑而不...
    開封第一講書人閱讀 58,147評(píng)論 1 292
  • 正文 為了忘掉前任狸捅,我火速辦了婚禮,結(jié)果婚禮上累提,老公的妹妹穿的比我還像新娘尘喝。我一直安慰自己,他們只是感情好斋陪,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,160評(píng)論 6 388
  • 文/花漫 我一把揭開白布朽褪。 她就那樣靜靜地躺著,像睡著了一般鳍贾。 火紅的嫁衣襯著肌膚如雪鞍匾。 梳的紋絲不亂的頭發(fā)上交洗,一...
    開封第一講書人閱讀 51,115評(píng)論 1 296
  • 那天骑科,我揣著相機(jī)與錄音,去河邊找鬼构拳。 笑死咆爽,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的置森。 我是一名探鬼主播斗埂,決...
    沈念sama閱讀 40,025評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼凫海!你這毒婦竟也來了呛凶?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,867評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤行贪,失蹤者是張志新(化名)和其女友劉穎漾稀,沒想到半個(gè)月后模闲,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,307評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡崭捍,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,528評(píng)論 2 332
  • 正文 我和宋清朗相戀三年尸折,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片殷蛇。...
    茶點(diǎn)故事閱讀 39,688評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡实夹,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出粒梦,到底是詐尸還是另有隱情亮航,我是刑警寧澤,帶...
    沈念sama閱讀 35,409評(píng)論 5 343
  • 正文 年R本政府宣布匀们,位于F島的核電站塞赂,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏昼蛀。R本人自食惡果不足惜宴猾,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,001評(píng)論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望叼旋。 院中可真熱鬧仇哆,春花似錦、人聲如沸夫植。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,657評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽详民。三九已至延欠,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間沈跨,已是汗流浹背由捎。 一陣腳步聲響...
    開封第一講書人閱讀 32,811評(píng)論 1 268
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留饿凛,地道東北人狞玛。 一個(gè)月前我還...
    沈念sama閱讀 47,685評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像涧窒,于是被迫代替她去往敵國(guó)和親心肪。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,573評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容