useEffect与ref对象使用指南

8,804 阅读3分钟

一、背景介绍

React Hooks中我们可以使用useEffect模拟Class组件的生命周期,而ref对象能帮助我们获取组件实例(也就是DOM元素)。但两者同时使用时,则会出现一些奇奇怪怪的问题。比如现有一个<App/>组件如下:

function useLoading() {
  const [loading, setLoading] = useState(false);
  useEffect(() => {
    setLoading(true);
  }, [])

  return loading;
}

const App = (props) => {
  const loading = useLoading();

  return (
    <div>
      <div>ref with useEffect</div>
        {loading && <div>...</div>}
    </div>
  )
}

整体逻辑就是在<App/>组件挂载完成之后,将loading设置为true,并显示新内容。假设我们想获取新内容的ref对象

function useLoading() {
  const [loading, setLoading] = useState(false);
  useEffect(() => {
    setLoading(true);
  }, [])

  return loading;
}

function useRefObject() {
  const ref = useRef();
  const [ready, setReady] = useState(false);

  useEffect(() => {
    if(ref.current) {
      setReady(true);
    }
  }, [ref])

  return [ref, ready];
}

const App = (props) => {
  const loading = useLoading();
  const [ref, ready] = useRefObject();

  return (
    <div>
      <div>ref with useEffect</div>
        {loading && <div ref={ref}>{ready.toString()}</div>}
    </div>
  )
}

上述代码预期的结果是:

  1. <App/>组件挂载完成之后,将loading设置为true
  2. 渲染<div />组件内容,并获取该组件ref对象
  3. ref对象更新之后,更新ready状态为true

但实际结果是这样

实际结果

二、分析原因

在网上找了一圈,发现挺多人也遇到了这个问题。个人总结就是,ref对象只会在组件渲染完成之后更新,而上述代码useRefObject中使用的useEffect是针对于整个<App/>组件的,所以它只会在两种情况下执行:

  • <App/>组件挂载成功之后(componentDidMount),此时loadingfalsediv内容不会渲染,ref为空
  • ref更新之后(componentDidUpdate),但ref仅在渲染完成后更新,而useEffect会在渲染过程中比较监听对象ref,因此在这就无法捕获

三、解决办法

知道问题所在之后就好办了,目前找到了三种可行的方法

3.1 添加监听变量

由于码useRefObject中定义的useEffect是针对于整个<App/>组件的,我们就可以将loading状态一起传入,并进行监听,这样loading的改变,在渲染<div/>组件的同时,也能被useEffect监听到

function useRefObject(loading) {
  const ref = useRef();
  const [ready, setReady] = useState(false);

  useEffect(() => {
    if(ref.current) {
      setReady(true);
    }
  }, [ref, loading])

  return [ref, ready];
}

3.2 将<div/>独立成一个新的组件

如果我们将<div />独立成一个新的组件,并在其内部定义useRefObject,这样就可以在该组件挂载后获取到ref对象了。不过这样做的缺陷是如果父组件<App />需要获取ref对象就很麻烦,同时一个简单的<div />独立成一个新组件,增加了无谓的开销

3.3 ref回调

这也是React官方推荐的方法

简单来说我们使用回调函数的方式,获取实时的ref对象

function useLoading() {
  const [loading, setLoading] = useState(false);
  useEffect(() => {
    setLoading(true);
  }, [])

  return loading;
}

function useRefCallback() {
  const [ready, setReady] = useState(false);
  const refCallback = useCallback(node => {
    if(node) {
      setReady(true)
    }
  }, []);

  return [ready, refCallback];
}

const App = (props) => {
  const loading = useLoading();
  const [ready, refCallback] = useRefCallback();

  return (
    <div>
      <div>ref with useEffect</div>
        {loading && <div ref={refCallback}>{ready.toString()}</div>}
    </div>
  )
}

四、参考资料