作者:Frost前言校对:袋鼠云栈前端团队运营组本文包含以下内容受控组件和非受控组件非受控组件受控组件和非受控组件受控组件边界反模式解决方案HTML,表单元素(//)通常维护自己的状态并根据用户输入更新name:在这个HTML中,我们可以在input中输入任意值,如果我们需要获取当前input输入的内容,应该怎么做我们的确是?ControlledanduncontrolledcomponentsUncontrolledcomponents(非受控组件)使用非受控组件,不是每次状态更新都写数据处理函数,而是将表单数据交给DOM节点处理,可以使用Ref获取数据在非受控组件中,希望能够给表单一个初始值,但不能控制后续的更新。DefaultValue可以用来指定一个默认值classFormextendsComponent{handleSubmitClick=()=>{constname=this._name.value;//用`name`做一些事情}render(){return(this._name=input}/>注册);}}受控组件(controlledcomponent)在React中,可变状态通常存储在组件的state属性中,只能通过setState进行更新classNameFormextendsReact.Component{constructor(props){super(props);this.state={value:'shuangxu'};}render(){return(name:表单>);}}上面代码中,在Input中设置了value属性值,所以显示的值始终是this.state.value,这使得state成为唯一的数据源consthandleChange=(event)=>{this.setState({value:event.target.value})}如果我们在上面的例子中写了handleChange方法,那么每次按键按下时,都会执行该方法,更新React的状态,所以表单的值会随着用户的输入而变化。React组件在用户输入的过程中控制表单的运行,状态仍然是唯一的数据源,这样通过React控制取值的表单输入元素称为受控组件。受控和非受控组件边界。不受控制的组件输入组件仅接收默认值。Input组件调用时,通过props传递一个defaultValue//componentfunctionInput({defaultValue}){return}//callfunctionDemo(){return}被控组件的值的显示和变化需要通过state和setState来控制。组件内部控制状态,实现自己的onChange方法//componentfunctionInput(){const[value,setValue]=useState('shuangxu')returnsetValue(e.target.value)}/>;}//调用函数Demo(){return;}此时Input组件是受控还是不受控??如果我们用前面的写法改变这个组件及其调用//组件函数Input({defaultValue}){const[value,setValue]=useState(defaultValue)returnsetValue(e.target.value)}/>;}//调用函数Demo(){return;}this当Input组件本身是一个受控组件时,它是由唯一的状态数据驱动的,但是对于Demo来说,我们没有Input组件的数据更改权,所以对于Demo组件来说,Input组件是一个非受控的控制元件。(??以非受控组件的方式调用受控组件是反模式)如何修改当前Input和Demo组件代码,使Input组件本身也是受控组件,自己也是受控组件对于演示组件ControlledNe?functionInput({value,onChange}){returnsetValue(e.target.value)}/>反模式——调用受控组件的方式与不受控组件相同描述其数据经常更新的组件。通过上一节受控组件和非受控组件的边界划分,我们可以简单的归类为:如果使用props传入数据,则有相应的数据处理方式,组件认为对父级可控的数据是只是保存在组件内部的状态,组件对于父级来说是不受控制的??什么是派生状态简单的说,如果一个组件状态中的某个数据来自外部,那么这个数据就称为派生状态。使用derivedstate导致的问题大部分无外乎两个原因:直接copypropstostate,如果props和state不一致,更新state和直接copypropstostate是否有变化,这两个生命周期会被执行,所以在这两个方法中直接copyprops到state是不安全的,会导致state不能正确渲染this.state={email:this.props.email//初始值为props中的email};}componentWillReceiveProps(nextProps){this.setState({email:nextProps.email});//更新时,重新分配状态}handleChange=(e)=>{this.setState({email:e.target.value});};render(){const{email}=this.state;return;}}点击查看示例为Input设置props的初始值,当Input进入时会修改状态。但是如果父组件重新渲染,输入框Input的值就会丢失,成为props的默认值。即使我们在reset之前比较nextProps.email!==this.state.email,对于当前的小demo,还是会引起update,可以使用shouldComponentUpdate比较props中的email是否被修改,然后判断是否重新渲染。但对于实际应用来说,这种做法是行不通的。一个组件会收到多个props,prop的任何变化都会导致重新渲染和不正确的状态重置。添加内联函数和对象道具,创建一个完全可靠的shouldComponentUpdate变得越来越困难。shouldComponentUpdate的生命周期更多是为了性能优化,而不是处理派生状态。至此,解释为什么不能直接将prop复制到state。思考另外一个问题,如果只使用props中的email属性来更新组件呢?修改props改变后的状态继续上面的例子,只使用props.email更新组件,可以防止修改状态导致的错误EmailInputextendsReact.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变化时,重新赋值状态}}//...}通过这种改造,组件只会在props.email变化时重新赋值状态,那么这种改造会不会有什么问题呢?在下面的场景中,在两个邮箱相同的账号之间切换时,输入框不会被重置,因为从父组件传过来的prop值没有改变。单击以查看示例。这个场景是构造出来的,设计的可能比较奇怪,但是像这样的错误是很常见的。对于这种反模式,这些问题有两种解决方案。关键是任何数据都必须只有一个数据源,切忌直接复制。解决方案完全可控组件从EmailInput组件中移除状态,直接使用props获取值,将受控组件的控制权交给父组件。functionEmailInput(props){return}如果要保存临时值,需要父组件手动保存。具有密钥的不受控制的组件允许组件存储临时电子邮件状态。email的初始值仍然通过prop接受,但是改变后的值与prop无关。functionEmailInput(props){const[email,setEmail]=useState(props.email)returnsetEmail(e.target.value)}/>}在前面切换账户的例子,为了在不同的页面值之间切换,你可以使用key这个React特殊属性。当密钥发生变化时,React会创建一个新组件,而不是简单地更新现有组件(获取更多)。我们在渲染动态列表的时候经常会用到键值,这里就可以用到。点击查看示例每次id改变,EmailInput都会被重新创建,其状态会重置为最新的email值。可选方案是使用key属性,该属性会重置组件整个组件的状态。可以观察getDerivedStateFromProps和componentWillReceiveProps中id的变化,比较麻烦但是可行。点击查看示例classEmailInputextendsComponent{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});}}//...}使用实例方法重置非受控组件有两种方法,这两种方法都是在具有唯一标识值的情况下。如果没有合适的键值,你也想重新创建组件。第一种解决方案是生成一个随机值或增量值作为键值。另一种是使用示例方法强制重置内部状态。父组件使用ref调用此方法。点击查看示例classEmailInputextendsComponent{state={email:this.props.email};resetEmailForNewUser(newEmail){this.setState({email:newEmail});}//...}那我们在业务开发中如何选择,尽量选择受控组件,减少派生状态的使用,过度使用componentWillReceiveProps可能会导致props判断不全,反而重复渲染死循环问题。在组件库的开发中,比如AntDesign,将受控和非受控两种调用方式都开放给用户,让用户自主选择对应的调用方式。例如,对于Form组件,我们经常使用getFieldDecorator和initialValue来定义表单项,但我们根本不关心中间的输入过程,在最终提交时通过getFieldsValue或validateFields获取所有的表单值。这是一种不受控制的调用方法。或者,当我们只有一个Input时,我们可以直接绑定value和onChange事件,以受控的方式调用。小结本文首先介绍了非受控组件和受控组件的概念。对于受控组件,组件控制用户输入的过程,状态是受控组件的唯一数据源。然后引入了组件调用的问题,对于组件调用者来说,组件提供者是否是受控组件。对于调用者来说,组件受控和非受控边界的划分取决于当前组件是否控制了子组件值的变化。然后介绍了像调用非托管组件一样调用受控组件的反模式用法,以及相关示例。不要将道具直接复制到状态,而是使用受控组件。对于非受控组件,当prop改变时想要重置状态,可以选择以下方法:建议:使用key属性在内部重置所有初始状态方案一:只改变部分字段,观察特殊属性变化(唯一attributes)方案二:使用ref调用实例方法最后总结一下如何选择受控组件和非受控组件。参考链接React官网-受控组件React官网-不受控组件controlledvs.uncontrolledforminputsTransitionfromuncontrolledinputtocontrolledRe-recognizecontrolleduncontrolledcomponentsYoumayneednotusederivedstate
重新认识受控组件和非受控组件相关文章