使用`useRef`获取Dom时,由于渲染条件无法在初次渲染拿到Dom

3,987 阅读2分钟

近日,我在 react 中封装一个"划词翻译功能"时,遇到一个使用 ref 的问题。解决了很长时间。

我们平常在使用 useRef 获取Dom元素,并且直接在 useEffect 中,立马就可以使用

 function Ap() {
   const ref = React.useRef();
  
   React.useEffect(() => {
       // 通常这里是可以直接获取到的
       console.log(ref.current); // input Dom
   }, []);
  
   return (
     <label>
       <input type="text" ref={ref} />
     </label>
   );
 }

但是,我遇到的情况是这样的(手撸代码,不必运行,好好看)

 function App() {
   const [state, setState] = useState(false);
   
   useEffect(() => {
     // 异步加载数据后
     setState(true);
   }, [])
 ​
   const ref = useRef();
  
   useEffect(() => {
       // 拿不到了吧
       console.log(ref.current); // undefined
   }, []);
  
   // jsx中条件未满足,所以没有createElement,所以拿不到ref
   return (
     <label>
       {state && <input type="text" ref={ref} />}
     </label>
   );
 }

那么,我们在封装功能的时候,需要用 ref (一开始就要拿,且保证只拿一次) 怎么办呢?

  • 使用 callbackRef 用回调函数的方式获取 ref,进而保存使用

    因为 callbackRef 会在真实Dom生成时执行

官网提示:

  • React will call the ref callback with the DOM element when the component mounts, and call it with null when it unmounts.
 function App() {
   const [state, setState] = useState(false);
   
   useEffect(() => {
     // 异步加载数据后
     setState(true);
   }, [])
 ​
   const domRef = useRef();
 ​
   const onMouseUp = async (e) => {
     console.log(e);
   };
 ​
   // 通过 callbackRef,在组件挂载后,操作该DOM
   // 注意:还要用useCallback来包装函数,不是为了性能优化,
   // 而是为了不生成新的回调函数让diff对比时发现差异,再次执行回调
   const callbackRef = useCallback((ref) => {
     // 并且卸载组件时,会再次传入null调用该函数,会引发报错
     // 所以:遇到null阻止运行
     if (!ref) return;
     // 给dom绑定事件
     ref.addEventListener("mouseup", onMouseUp);
     // 保留ref,便于组件卸载时清除副作用
     domRef.current = ref;
   }, []);
 ​
   useEffect(() => {
     return () => {
       // 清除事件
       domRef.current.removeEventListener("mouseup", onMouseUp);
     };
   }, []);
  
   // jsx中条件未满足,所以没有createElement,所以拿不到ref
   return (
     <label>
       {state && <input type="text" ref={callbackRef} />}
     </label>
   );
 }
  • 注意:

    • 回调函数要用 useCallback 缓存,避免生成新的 ref 让Diff对比时,以为发现新的差异
    • 卸载组件时,会再次传入null调用 callbackRef,要注意规避报错

总结:

  • callbackRef 相较于 直接给值 的方式还是有一些特殊用处的。