乐趣区

关于javascript:React新文档不要滥用Ref哦~

大家好,我卡颂。

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 传递的数据结构。

退出移动版