当前位置: 首页 > 科技观察

React新文档:不要滥用Ref!

时间:2023-03-13 19:32:47 科技观察

大家好,我是Kason。新的React文档有一个非常有趣的细节:引入了useRef和useEffect两个API,文档中的章节叫做EscapeHatches(逃生舱口)。显然,在正常航行期间不需要逃生舱,只有在遇到危险时才需要。如果开发者过分依赖这两个API,则可能是误用。在新的React文档:Don'tabuseeffect中,我们谈到了useEffect的正确使用场景。今天,我们就来说说Ref.的??使用场景。为什么是逃生舱?先思考一个问题:为什么ref和effect被归类为“逃生舱口”?这是因为两者都对“React无法控制的因素”起作用。实际上处理的是“副作用”。例如:修改useEffect中的document.title。document.title在React中不属于state,React无法感知它的变化,所以归类到effect中。同样,“聚焦DOM”需要调用element.focus(),直接执行DOMAPI不受React控制。虽然它们是“React无法控制的因素”,但为了保证应用程序的健壮性,React应该尽量避免它们失控。Ref失控对于Ref,什么是失控?我们先来看“没有失控”的情况:执行ref.current的focus、blur等方法。执行ref.current.scrollIntoView使元素滚动到视图中。执行ref.current.getBoundingClientRect以测量DOM大小。在这些情况下,虽然我们对DOM进行了操作,但涉及到“React无法控制的因素”,所以并不是失控。但是下面的情况:执行ref.current.remove来移除DOM。执行ref.current.appendChild插入子节点。同样是操作DOM,但这些都是“React控制范围内的因素”,通过ref进行这些操作是不受控制的。比如下面是React文档[1]中的例子:“Button1”点击后会插入/移除P节点,“Button2”点击后会调用DOMAPI移除P节点:exportdefault函数Counter(){const[show,setShow]=useState(true);constref=useRef(null);return(

{setShow(!show);}}>使用setState切换{ref.current.remove();}}>从DOM中移除{show&&Helloworld

}
);}“Button1”通过React控件移除P节点。“按钮2”直接操作DOM移除P节点。如果这两种“移除P节点”的方法混用的话,那么先点击“按钮1”再点击“按钮2”会报错:这是“使用Ref操作DOM导致失控”造成的。如何限制失控现在问题来了,既然叫“失控”,那么React是无法控制的(React不能限制开发者使用DOMAPI对吧?),那么如何极限失控?在React中,组件可以分为高层组件和低层组件。“低级组件”是指那些“基于DOM的组件”。例如下面的组件是直接基于input节点封装的:示例:functionMyInput(props){constref=useRef(null);return;}“高层组件”是指那些“基于低层组件封装的组件”,比如下面的Form组件,就是基于输入组件:functionForm(){return(<>)}“高阶组件”不能直接将ref指向DOM。这个限制会把“ref失控”的范围控制在单个组件内,不会出现跨组件的“ref失控”。以文档中的例子[2]为例,如果我们要点击Form组件中的按钮,操作输入焦点:functionMyInput(props){return;}functionForm(){constinputRef=useRef(null);函数handleClick(){inputRef.current.focus();}return(<>输入焦点);}点击后会报错:这是因为ref的传递Form组件中的MyInput失败,并且inputRef.current没有指向输入节点。原因是“为了控制单个组件内ref的失控范围,React默认不支持跨组件传递ref”。人为取消限制如果一定要取消这个限制,可以使用forwardRefAPI显式传递ref:constMyInput=forwardRef((props,ref)=>{return;});functionForm(){constinputRef=useRef(null);函数handleClick(){inputRef.current.focus();}return(<>focustheinput);}使用forwardRef后(这里forward是“传递”的意思),可以通过跨组件引用。在示例中,我们将inputRef从Form传递到MyInput并将其与输入相关联。在实践中,可能有同学会觉得forwardRefAPI有点多余。但从“ref失控”的角度来看,forwardRef的用意很明显:开发者既然手动调用forwardRef打破了“ref失控的限制”,他就应该知道自己在做什么,应该承担相应的风险.同时,有了forwardRef的存在,在出现“ref-relatederrors”后更容易定位错误。除了“限制ref的跨组件传递”之外,useImperativeHandle还有一个“防止ref失控的措施”,那就是useImperativeHandle,它的逻辑是这样的:既然“ref失控”是因为“使用一个不应该使用的DOM方法”(比如appendChild),那么我可以限制“只有ref中可以使用的方法”。使用useImperativeHandle修改我们的MyInput组件:},}));返回;});现在,Form组件只能通过inputRef.current获取如下数据结构:{focus(){realInputRef.当前.focus();},}防止出现“开发者通过ref获取DOM,执行不该使用的API,ref失控”的情况。总结一下正常情况,Ref很少被使用,它作为一个“逃生门”存在。为了防止ref因误用/滥用而失控,React限制“默认情况下,ref不能跨组件传递”。为了摆脱这个限制,可以使用forwardRef。为了减少ref对DOM的滥用,可以使用useImperativeHandle来限制ref传递的数据结构。参考文献[1]React文档中的示例:https://codesandbox.io/s/sandpack-project-forked-s33q3c。[2]文档中的示例:https://codesandbox.io/s/sandpack-project-forked-7zqgmd。