全国免费咨询:

13245491521

VR图标白色 VR图标黑色
X

中高端软件定制开发服务商

与我们取得联系

13245491521     13245491521

2024-05-08_「转」深入理解受控组件、非受控组件

您的位置:首页 >> 新闻 >> 行业资讯

深入理解受控组件、非受控组件 点击关注公众号,“技术干货”及时达! 前端开发经常会涉及表单的处理,或者其他一些用于输入的组件,比如日历组件。 涉及到输入,就绕不开受控模式和非受控模式的概念。 什么是受控,什么是非受控呢? 想一下,改变表单值只有两种情况: 用户去改变 value 或者代码去改变 value。 如果不能通过代码改表单值 value,那就是非受控,也就是不受我们控制。 但是代码可以给表单设置初始值 defaultValue。 代码设置表单的初始 value,但是能改变 value 的只有用户,代码通过监听 onChange 来拿到最新的值,或者通过 ref 拿到 dom 之后读取 value。 这种就是非受控模式。 反过来,代码可以改变表单的 value,就是受控模式。 注意,value 和 defaultValue 不一样: defaultValue 会作为 value 的初始值,后面用户改变的是 value。 而一旦你给 input 设置了 value,那用户就不能修改它了,可以输入触发 onChange 事件,但是表单的值不会变。 用户输入之后在 onChange 事件里拿到输入,然后通过代码去设置 value。 这就是受控模式。 其实绝大多数情况下,非受控就可以了,因为我们只是要拿到用户的输入,不需要手动去修改表单值。 但有的时候,你需要根据用户的输入做一些处理,然后设置为表单的值,这种就需要受控模式。 或者你想同步表单的值到另一个地方的时候,类似 Form 组件,也可以用受控模式。 「value 由用户控制就是非受控模式,由代码控制就是受控模式」。 我们写代码试一下: npx create-vite 创建 vite + react 的项目。 去掉 main.tsx 的 index.css 和 StrictMode: 改下 App.tsx import{ChangeEvent}from"react" functionApp(){ functiononChange(event:ChangeEventHTMLInputElement){ console.log(event.target.value); } returninputdefaultValue={'guang'}onChange={onChange}/ } exportdefaultApp 跑一下开发服务: npm install npm run dev 看下效果: defaultValue 作为 value 的初始值,然后用户输入触发 onChange 事件,通过 event.target 拿到了 value。 当然,非受控模式也不一定通过 onChange 拿到最新 value,通过 ref 也可以。 import{useEffect,useRef}from"react" functionApp(){ constinputRef=useRefHTMLInputElement(null); useEffect(()={ setTimeout(()={ console.log(inputRef.current?.value); },2000); }, returninputdefaultValue={'guang'}ref={inputRef}/ } exportdefaultApp 接下来看下受控模式的写法: import{ChangeEvent,useState}from"react" functionApp(){ const[value,setValue]=useState('guang'); functiononChange(event:ChangeEventHTMLInputElement){ console.log(event.target.value); //setValue(event.target.value); } returninputvalue={value}onChange={onChange}/ } exportdefaultApp 我们先把 setValue 注释掉,看下用户可不可以改: 可以看到,用户可以输入,onChange 也可以拿到输入后的表单值,但是 value 并没有变。 把 setValue 那一行注释去掉就可以了。 虽然功能上差不多,但这种写法并不推荐: 你不让用户自己控制,而是通过代码控制,绕了一圈结果也没改 value 的值,还是原封不动的,图啥呢? 而且受控模式每次 setValue 都会导致组件重新渲染。 试一下: 每次输入都会 setValue,然后触发组件重新渲染: 而非受控模式下只会渲染一次: 绕了一圈啥也没改,还导致很多组件的重新渲染,那你用受控模式图啥呢? 那什么情况用受控模式呢? 当然是你「需要对输入的值做处理之后设置到表单的时候,或者是你想实时同步状态值到父组件。」 比如把用户输入改为大写: import{ChangeEvent,useState}from"react" functionApp(){ const[value,setValue]=useState('guang'); functiononChange(event:ChangeEventHTMLInputElement){ console.log(event.target.value) setValue(event.target.value.toUpperCase()); } returninputvalue={value}onChange={onChange}/ } exportdefaultApp 这种,需要把用户的输入修改一下再设置 value 的。 但这种场景其实很少。 有的同学可能会说 Form 组件,确实,用 Form.Item 包裹的表单项都是受控组件: 确实,那是因为 Form 组件内有一个 Store,会把表单值同步过去,然后集中管理和设置值: 但也因为都是受控组件,随着用户的输入,表单重新渲染很多次,性能会不好。 如果是单独用的组件,比如 Calendar,那就没必要用受控模式了,用非受控模式,设置 defaultValue 就可以了。 很多人上来就设置 value,然后监听 onChange,但是绕了一圈又原封不动的把用户输入转为 value。 没啥意义,还平白导致组件的很多次重新渲染。 除了原生表单元素外,组件也需要考虑受控和非受控的情况。 比如日历组件: 它的参数就要考虑是支持非受控模式的 defaultValue,还是用受控模式的 value + onChange。 如果这是一个业务组件,那基本就是用非受控模式的 defaultValue 了,调用方只要拿到用户的输入就行。 用受控模式的 value 还要 setValue 触发额外的渲染。 但是基础组件不能这样,你得都支持,让调用者自己去选择。 ant design 的 Calendar 组件就是这样的: ColorPicker 组件也是: 它同时支持了受控组件和非受控组件。 咋做到的呢? 我们来试试: 首先写下非受控组件的写法: import{ChangeEvent,useState}from"react" interfaceCalendarProps{ defaultValue?:Date; onChange?:(date:Date)=void; } functionCalendar(props:CalendarProps){ const{ defaultValue=newDate(), onChange }=props; const[value,setValue]=useState(defaultValue); functionchangeValue(date:Date){ setValue(date); onChange?.(date); } returndiv {value.toLocaleDateString()} divonClick={()={changeValue(newDate('2024-5-1'))}}2023-5-1/div divonClick={()={changeValue(newDate('2024-5-2'))}}2023-5-2/div divonClick={()={changeValue(newDate('2024-5-3'))}}2023-5-3/div /div } functionApp(){ returnCalendardefaultValue={newDate('2024-5-1')}onChange={(date)={ console.log(date.toLocaleDateString()); }}/ } exportdefaultApp 这里 Calendar 组件传入 defaultValue 和 onChange 参数。 defaultValue 会作为 value 的初始值,然后用户点击不同日期会修改 value,然后回调 onChange 函数。 这种情况,调用者只能设置 defaultValue 初始值,不能直接修改 value,所以是非受控模式。 试一下; 然后再来写下受控模式的版本: import{ChangeEvent,useEffect,useState}from"react" interfaceCalendarProps{ value:Date; onChange?:(date:Date)=void; } functionCalendar(props:CalendarProps){ const{ value, onChange }=props; functionchangeValue(date:Date){ onChange?.(date); } returndiv {value.toLocaleDateString()} divonClick={()={changeValue(newDate('2024-5-1'))}}2023-5-1/div divonClick={()={changeValue(newDate('2024-5-2'))}}2023-5-2/div divonClick={()={changeValue(newDate('2024-5-3'))}}2023-5-3/div /div } functionApp(){ const[value,setValue]=useState(newDate('2024-5-1')); returnCalendarvalue={value}onChange={(date)={ console.log(date.toLocaleDateString()); setValue(date); }}/ } exportdefaultApp 直接用 props 传入的 value,然后切换日期的时候回调 onChange 函数: value 的值的维护在调用方。 这就是受控组件的写法: 那能不能同时支持受控和非受控模式呢? 可以的,组件库基本都是这么做的: import{useEffect,useRef,useState}from"react" interfaceCalendarProps{ value?:Date; defaultValue?:Date; onChange?:(date:Date)=void; } functionCalendar(props:CalendarProps){ const{ value:propsValue, defaultValue, onChange }=props; const[value,setValue]=useState(()={ if(propsValue!==undefined){ returnpropsValue; }else{ returndefaultValue; } constisFirstRender=useRef(true); useEffect(()={ if(propsValue===undefined!isFirstRender.current){ setValue(propsValue); } isFirstRender.current=false; },[propsValue]); constmergedValue=propsValue===undefined?value:propsValue; functionchangeValue(date:Date){ if(propsValue===undefined){ setValue(date); } onChange?.(date); } returndiv {mergedValue?.toLocaleDateString()} divonClick={()={changeValue(newDate('2024-5-1'))}}2023-5-1/div divonClick={()={changeValue(newDate('2024-5-2'))}}2023-5-2/div divonClick={()={changeValue(newDate('2024-5-3'))}}2023-5-3/div /div } functionApp(){ returnCalendardefaultValue={newDate('2024-5-1')}onChange={(date)={ console.log(date.toLocaleDateString()); }}/ } exportdefaultApp 参数同时支持 value 和 defaultValue,通过判断 value 是不是 undefined 来区分受控模式和非受控模式。 如果是受控模式,useState 的初始值设置 props.value,然后渲染用 props.value。 如果是非受控模式,那渲染用内部 state 的 value,然后 changeValue 里 setValue。 当不是首次渲染,但 value 变为 undefined 的情况,也就是从受控模式切换到了非受控模式,要同步设置 state 为 propsValue。 这样,组件就同时支持了受控和非受控模式。 测试下: 非受控模式: 受控模式: 其实组件库也都是这么做的。 比如 arco design 的 useMergeValue 的 hook: 代码差不多,它也是 useState 根据 value 是不是 undefined 来设置 value 或者 defaultValue。 不过它这里又加了一个默认值,没有 defaultValue 的时候用它哪个 defaultStateValue。 然后渲染用的 state 根据 value 是不是 undefind 来判断受控非受控从而决定用 props 的 value 还是 state 的 value。 它也处理了 value 从别的值变为 undefined 的情况: 保存了之前的 value,判断是从 props.value 别的值变为 undefined 的情况再修改内部 state 为这个 value。 这里保存之前的值是用的 useRef: ref 的特点是修改了 current 属性不会导致渲染。 我们是判断非首次渲染,但是 props.value 变为了 undefined,效果一样。 再比如 ant design 的工具包 rc-util 里的 useMergedValue 的 hook: 它也是 useState 根据 value 是不是 undefined 来设置 value 或者 defaultValue 然后又加了一个默认值,没有 defaultValue 的时候用它那个 defaultStateValue。 渲染的时候也是判断 value 是不是 undefind 来决定用 props.value 还是 state 的 value: 并且也做了别的值变为 undefined 的处理。 大家都这么搞,我们也来封装个 hook: functionuseMergeStateT( defaultStateValue:T, props?:{ defaultValue?:T, value?:T } ):[T,React.DispatchReact.SetStateActionT,]{ const{defaultValue,value:propsValue}=props|| constisFirstRender=useRef(true); const[stateValue,setStateValue]=useState(()={ if(propsValue!==undefined){ returnpropsValue!; }elseif(defaultValue!==undefined){ returndefaultValue!; }else{ returndefaultStateValue; } useEffect(()={ if(propsValue===undefined!isFirstRender.current){ setStateValue(propsValue!); } isFirstRender.current=false; },[propsValue]); constmergedValue=propsValue===undefined?stateValue:propsValue; return[mergedValue,setStateValue] } 用一下: interfaceCalendarProps{ value?:Date; defaultValue?:Date; onChange?:(date:Date)=void; } functionCalendar(props:CalendarProps){ const{ value:propsValue, defaultValue, onChange }=props; const[mergedValue,setValue]=useMergeState(newDate(),{ value:propsValue, defaultValue functionchangeValue(date:Date){ if(propsValue===undefined){ setValue(date); } onChange?.(date); } returndiv {mergedValue?.toLocaleDateString()} divonClick={()={changeValue(newDate('2024-5-1'))}}2023-5-1/div divonClick={()={changeValue(newDate('2024-5-2'))}}2023-5-2/div divonClick={()={changeValue(newDate('2024-5-3'))}}2023-5-3/div /div } 试试效果: 非受控模式: 受控模式: 再就是这个 onChange 部分,也应该封装进来: 不然用户用的时候还要想着去处理非受控组件的情况。 我看 arco design 里是没封装进去: 但是 ahooks 的 useControllableValue 就封装进去了: 我们也加一下: import{SetStateAction,useCallback,useEffect,useRef,useState}from"react" functionuseMergeStateT( defaultStateValue:T, props?:{ defaultValue?:T, value?:T, onChange?:(value:T)=void; }, ):[T,React.DispatchReact.SetStateActionT,]{ const{defaultValue,value:propsValue,onChange}=props|| constisFirstRender=useRef(true); const[stateValue,setStateValue]=useState(()={ if(propsValue!==undefined){ returnpropsValue!; }elseif(defaultValue!==undefined){ returndefaultValue!; }else{ returndefaultStateValue; } useEffect(()={ if(propsValue===undefined!isFirstRender.current){ setStateValue(propsValue!); } isFirstRender.current=false; },[propsValue]); constmergedValue=propsValue===undefined?stateValue:propsValue; functionisFunction(value:unknown):valueisFunction{ returntypeofvalue==='function'; } constsetState=useCallback((value:SetStateAction)={ letres=isFunction(value)?value(stateValue):value if(propsValue===undefined){ setStateValue(res); } onChange?.(res); },[stateValue]); return[mergedValue,setState] } interfaceCalendarProps{ value?:Date; defaultValue?:Date; onChange?:(date:Date)=void; } functionCalendar(props:CalendarProps){ const{ value:propsValue, defaultValue, onChange }=props; const[mergedValue,setValue]=useMergeState(newDate(),{ value:propsValue, defaultValue, onChange returndiv {mergedValue?.toLocaleDateString()} divonClick={()={setValue(newDate('2024-5-1'))}}2023-5-1/div divonClick={()={setValue(newDate('2024-5-2'))}}2023-5-2/div divonClick={()={setValue(newDate('2024-5-3'))}}2023-5-3/div /div } functionApp(){ const[value,setValue]=useState(newDate('2024-5-1')); returnCalendarvalue={value}onChange={(date)={ console.log(date.toLocaleDateString()); setValue(date); }}/ //returnCalendardefaultValue={newDate('2024-5-1')}onChange={(date)={ //console.log(date.toLocaleDateString()); //}}/ } exportdefaultApp 这里把 onChange 传入了,然后 setState 的时候拿到新的状态值,如果是非受控模式就 setStateValue,然后调用 onChange。 这里要拿到之前的 value 值,考虑闭包陷阱的问题,所以用 useCallback 加上 stateValue 作为依赖来解决。 用的时候就不用区分受控非受控了,直接 setState 就行: 试试效果: 非受控模式: 受控模式: 完美! 这样,我们的组件就同时支持了受控模式和非受控模式。 案例代码上传了react 小册仓库:https://github.com/QuarkGluonPlasma/react-course-code/tree/main/controlled-and-uncontrolled。 总结涉及到用户输入的组件都要考虑用受控模式还是非受控模式。 「value 由用户控制就是非受控模式,由代码控制就是受控模式」。 非受控模式就是完全用户自己修改 value,我们只是设置个 defaultValue,可以通过 onChange 或者 ref 拿到表单值。 受控模式是代码来控制 value,用户输入之后通过 onChange 拿到值然后 setValue,触发重新渲染。 单独用的组件,绝大多数情况下,用非受控模式就好了,因为你只是想获取到用户的输入。 受控模式只在需要对用户的输入做一些修改然后再设置到 value 的情况用,再就是实时同步表单值到父组件的时候,比如 Form。 如果需要结合 Form 表单用,那是要支持受控模式,因为 Form 会通过 Store 来统一管理所有表单项。 封装业务组件的话,用非受控模式或者受控都行。 但是基础组件还是都要支持,也就是支持 defaultValue 和 value + onChange 两种参数,内部通过判断 value 是不是 undefined 来区分。 写组件想同时支持受控和非受控,可以直接用 ahooks 的 useControllableValue,也可以自己实现。 arco design、ant design 等组件库都是这么做的,并且不约而同封装了 useMergedValue 的 hook,我们也封装了一个。 理清受控模式和非受控模式的区别,在写组件的时候灵活选用或者都支持。 ?更多内容可以看我的小册《React 通关秘籍》 ? 阅读原文

上一篇:2020-05-06_中奖彩票,子网络的觉悟 下一篇:2024-08-29_奥运营销策略,5个观察和思考

TAG标签:

20
网站开发网络凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设网站改版域名注册主机空间手机网站建设网站备案等方面的需求...
请立即点击咨询我们或拨打咨询热线:13245491521 13245491521 ,我们会详细为你一一解答你心中的疑难。
项目经理在线

相关阅读 更多>>

猜您喜欢更多>>

我们已经准备好了,你呢?
2022我们与您携手共赢,为您的企业营销保驾护航!

不达标就退款

高性价比建站

免费网站代备案

1对1原创设计服务

7×24小时售后支持

 

全国免费咨询:

13245491521

业务咨询:13245491521 / 13245491521

节假值班:13245491521()

联系地址:

Copyright © 2019-2025      ICP备案:沪ICP备19027192号-6 法律顾问:律师XXX支持

在线
客服

技术在线服务时间:9:00-20:00

在网站开发,您对接的直接是技术员,而非客服传话!

电话
咨询

13245491521
7*24小时客服热线

13245491521
项目经理手机

微信
咨询

加微信获取报价