大家好,我卡颂。

React新文档有个很有意思的细节:useRefuseEffect这两个API的介绍,在文档中所在的章节叫Escape Hatches(逃生舱)。

显然,失常航行时是不须要逃生舱的,只有在遇到危险时会用到。

如果开发者过多依赖这两个API,可能是误用。

在React新文档:不要滥用effect哦中咱们谈到useEffect的正确应用场景。

明天,咱们来聊聊Ref的应用场景。

欢送退出人类高质量前端框架群,带飞

为什么是逃生舱?

先思考一个问题:为什么refeffect被归类到逃生舱中?

这是因为二者操作的都是脱离React管制的因素

effect中解决的是副作用。比方:在useEffect中批改了document.title

document.title不属于React中的状态,React无奈感知他的变动,所以被归类到effect中。

同样,使DOM聚焦须要调用element.focus(),间接执行DOM API也是不受React管制的。

尽管他们是脱离React管制的因素,但为了保障利用的强壮,React也要尽可能避免他们失控。

失控的Ref

对于Ref,什么叫失控呢?

首先来看不失控的状况:

  • 执行ref.currentfocusblur等办法
  • 执行ref.current.scrollIntoView使element滚动到视线内
  • 执行ref.current.getBoundingClientRect测量DOM尺寸

这些状况下,尽管咱们操作了DOM,但波及的都是React管制范畴外的因素,所以不算失控。

然而上面的状况:

  • 执行ref.current.remove移除DOM
  • 执行ref.current.appendChild插入子节点

同样是操作DOM,但这些属于React管制范畴内的因素,通过ref执行这些操作就属于失控的状况。

举个例子,上面是React文档中的例子:

按钮1点击后会插入/移除 P节点,按钮2点击后会调用DOM API移除P节点:

export default function Counter() {  const [show, setShow] = useState(true);  const ref = useRef(null);  return (    <div>      <button        onClick={() => {          setShow(!show);        }}>        Toggle with setState      </button>      <button        onClick={() => {          ref.current.remove();        }}>        Remove from the DOM      </button>      {show && <p ref={ref}>Hello world</p>}    </div>  );}

按钮1通过React管制的形式移除P节点。

按钮2间接操作DOM移除P节点。

如果这两种移除P节点的形式混用,那么先点击按钮1再点击按钮2就会报错:

这就是应用Ref操作DOM造成的失控状况导致的。

如何限度失控

当初问题来了,既然叫失控了,那就是React没法管制的(React总不能限度开发者不能应用DOM API吧?),那如何限度失控呢?

React中,组件能够分为:

  • 高阶组件
  • 低阶组件

低阶组件指那些基于DOM封装的组件,比方上面的组件,间接基于input节点封装:

function MyInput(props) {  return <input {...props} />;}

低阶组件中,是能够间接将ref指向DOM的,比方:

function MyInput(props) {  const ref = useRef(null);  return <input ref={ref} {...props} />;}

高阶组件指那些基于低阶组件封装的组件,比方上面的Form组件,基于Input组件封装:

function Form() {  return (    <>      <MyInput/>    </>  )}

高阶组件无奈间接将ref指向DOM,这一限度就将ref失控的范畴管制在单个组件内,不会呈现逾越组件的ref失控

以文档中的示例为例,如果咱们想在Form组件中点击按钮,操作input聚焦:

function MyInput(props) {  return <input {...props} />;}function Form() {  const inputRef = useRef(null);  function handleClick() {    inputRef.current.focus();  }  return (    <>      <MyInput ref={inputRef} />      <button onClick={handleClick}>        input聚焦      </button>    </>  );}

点击后,会报错:

这是因为在Form组件中向MyInput传递ref失败了,inputRef.current并没有指向input节点。

究其原因,就是下面说的为了将ref失控的范畴管制在单个组件内,React默认状况下不反对跨组件传递ref

人为勾销限度

如果肯定要勾销这个限度,能够应用forwardRef API显式传递ref

const MyInput = forwardRef((props, ref) => {  return <input {...props} ref={ref} />;});function Form() {  const inputRef = useRef(null);  function handleClick() {    inputRef.current.focus();  }  return (    <>      <MyInput ref={inputRef} />      <button onClick={handleClick}>        Focus the input      </button>    </>  );}

应用forwardRefforward在这里是传递的意思)后,就能跨组件传递ref

在例子中,咱们将inputRefForm跨组件传递到MyInput中,并与input产生关联。

在实践中,一些同学可能感觉forwardRef这一API有些多此一举。

但从ref失控的角度看,forwardRef的用意就很显著了:既然开发者手动调用forwardRef破除避免ref失控的限度,那他应该晓得本人在做什么,也应该本人承当相应的危险。

同时,有了forwardRef的存在,产生ref相干谬误后也更容易定位谬误。

useImperativeHandle

除了限度跨组件传递ref外,还有一种避免ref失控的措施,那就是useImperativeHandle,他的逻辑是这样的:

既然ref失控是因为应用了不该被应用的DOM办法(比方appendChild),那我能够限度ref中只存在能够被应用的办法

useImperativeHandle批改咱们的MyInput组件:

const MyInput = forwardRef((props, ref) => {  const realInputRef = useRef(null);  useImperativeHandle(ref, () => ({    focus() {      realInputRef.current.focus();    },  }));  return <input {...props} ref={realInputRef} />;});

当初,Form组件中通过inputRef.current只能取到如下数据结构:

{  focus() {    realInputRef.current.focus();  },}

就杜绝了开发者通过ref取到DOM后,执行不该被应用的API,呈现ref失控的状况。

总结

失常状况,Ref的应用比拟少,他是作为逃生舱而存在的。

为了避免错用/滥用导致ref失控React限度默认状况下,不能跨组件传递ref

为了破除这种限度,能够应用forwardRef

为了缩小refDOM的滥用,能够应用useImperativeHandle限度ref传递的数据结构。