一、背景介绍
在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>
)
}
上述代码预期的结果是:
- 在
<App/>组件挂载完成之后,将loading设置为true - 渲染
<div />组件内容,并获取该组件ref对象 ref对象更新之后,更新ready状态为true
但实际结果是这样

二、分析原因
在网上找了一圈,发现挺多人也遇到了这个问题。个人总结就是,ref对象只会在组件渲染完成之后更新,而上述代码useRefObject中使用的useEffect是针对于整个<App/>组件的,所以它只会在两种情况下执行:
<App/>组件挂载成功之后(componentDidMount),此时loading为false,div内容不会渲染,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>
)
}