React Hooks - useEffect 中无法使用 Ref

39
我正在使用ReactHooks。我试图在useEffect函数中访问User组件的ref,但是我得到了null的值,尽管我将elRef.current作为第二个参数传递给useEffect。我应该获得对元素的引用,但在useEffect之外(函数体外),ref值可用。为什么?如何在useEffect内获取elRef.current值? 代码:
import React, { Component, useState, useRef, useEffect } from "react";

const useFetch = url => {
  const [data, setData] = useState(null);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);

  useEffect(
    () => {
      setIsLoading(true);
      fetch(url)
        .then(response => {
          if (!response.ok) throw Error(response.statusText);
          return response.json();
        })
        .then(json => {
          setIsLoading(false);
          setData(json.data);
        })
        .catch(error => {
          setIsLoading(false);
          setError(error);
        });
    },
    [url]
  );

  return { data, isLoading, error };
};

const User = ({ id }) => {
  const elRef = useRef(null);
  const { data: user } = useFetch(`https://reqres.in/api/users/${id}`);

  useEffect(() => {
    console.log("ref", elRef.current);
  }, [elRef.current]);
  if (!user) return null;
  return <div ref={elRef}>{user.first_name + " " + user.last_name}</div>;
};

class App extends Component {
  state = {
    userId: 1
  };

  handleNextClick = () => {
    this.setState(prevState => ({
      userId: prevState.userId + 1
    }));
  };

  handlePrevNext = () => {
    this.setState(prevState => ({
      userId: prevState.userId - 1
    }));
  };
  render() {
    return (
      <div>
        <button
          onClick={() => this.handlePrevClick()}
          disabled={this.state.userId === 1}
        >
          Prevoius
        </button>
        <button onClick={() => this.handleNextClick()}>Next</button>
        <User id={this.state.userId} />
      </div>
    );
  }
}

export default App;
Codesandbox链接
谢谢!
6个回答

57

根据 reactjs 文档建议,您应该使用 useCallback 而不是 useRef。

当 ref 附加到不同的节点时,React 将调用该回调函数。

替换为以下内容:

const elRef = useRef(null);
useEffect(() => {
    console.log("ref", elRef.current);
}, [elRef.current]);

使用这个:

const elRef = useCallback(node => {
    if (node !== null) {
        console.log("ref", node); // node = elRef.current
    }
}, []);

不错!严格来说,useCallback钩子对于这个工作并不是必需的,因为没有依赖关系。elRef函数可以在组件外部定义,因此只会被创建一次。 - Trevedhek

8

当你使用一个 函数作为 ref 时,它在实例准备好时被调用。因此,将 ref 变成可观察的最简单的方法是使用 useState 而不是 useRef

const [element, setElement] = useState<Element | null>(null);
return <div ref={setElement}></div>;

接下来,您可以像处理任何其他 const 值一样,在依赖数组中使用它作为其他钩子的依赖项:

useEffect(() => {
  if (element) console.log(element);
}, [element]);

请参见如何在引用发生更改时重新渲染


1
这种方法相对于useCallback的优势在于可以操作元素。例如,element.scrollTop = number在这里可以工作,而在useCallback中则不行。 - Shaun Dychko

8

这是一种可预测的行为。

@estus所述,您会遇到这个问题,因为第一次在componentDidMount上调用它时,您会得到null(初始值),只有在下一个elRef更改时才会更新,因为实际上引用仍然相同。

如果您需要反映每次用户更改,则应将[user]作为第二个参数传递给函数,以确保useEffect在用户更改时触发。

这里是更新后的沙盒。

希望这有所帮助。


我以为 useEffect 是在渲染方法运行后被调用的?我不明白它为什么应该是 null。 - jayarjo
@jayarjo 是的,你说得对。在第一次渲染时,没有用户直到它从服务器获取,这就是为什么我们会得到这样的结果。 - Denys Kotsur
1
如果没有用户,则有一个条件来呈现null,因此引用仍然是初始化时的null - Denys Kotsur
好的,我错过了他实际上返回null的那一行。 - jayarjo

3
假设这里的前提是useEffect需要检测ref.current的更改,因此需要在依赖项列表中添加refref.current。我认为这是由于es-lint过于严格造成的。
实际上,useEffect的整个重点在于它保证不会在渲染完成和DOM准备就绪之前运行。这就是它处理副作用的方式。
因此,在执行useEffect时,我们可以确信elRef.current已经设置好了。
你的代码问题在于,直到user被填充后才使用<div ref={elRef}...>运行渲染器。因此,你想要的elRef引用的DOM节点尚不存在。这就是为什么你会得到null日志记录的原因 - 而不是依赖项的原因。
顺便说一句:一个可能的替代方案是在effect hook内部填充div。
useEffect(
  () => {
    if(!user) return;
    elRef.current.innerHTML = `${user.first_name} ${user.last_name}`;
  }, [user]
);
...
//if (!user) return null;// Remove this line
return <div ref={elRef}></div>; //return div every time.

这样,在用户组件中的if (!user) return null;行就是不必要的。将其删除,elRef.current保证从一开始就填充了div节点。


3

useEffect被用作componentDidMount和componentDidUpdate,当组件挂载时,你可以添加一个条件:

if (!user) return null;
return <div ref={elRef}>{user.first_name + " " + user.last_name}</div>;

由于挂载时的上述条件,您没有用户,因此返回null并且未在添加ref的DOM中安装div,因此在useEffect内部无法获取elRef的当前值,因为它没有呈现。
当单击下一个时,由于div已经安装在dom中,您可以获得elRef.current的值。

我们可以在初始渲染(解析后的获取)中看到用户名。这意味着用户存在。点击“下一个”按钮以获取下一个用户。 - REDDY PRASAD
@REDDYPRASAD,它在最初的渲染时不存在,试着移除那个条件,你的代码就会崩溃。这意味着它最初没有呈现那个div。 - Piyush Zalani
@REDDYPRASAD,如果您删除该条件,则会显示“无法读取null的属性'first_name'”错误。 - Piyush Zalani
@REDDYPRASAD,你可以执行 {!user ? "'':user.first_name + " "+ user.last_name} 并且移除对用户检查的条件。 - Piyush Zalani
因为我们不需要点击“下一页”按钮来获取用户的信息,所以被踩了。如果是这种情况,当页面加载时我们怎么看到用户名呢(而不需要点击“下一页”)? - REDDY PRASAD
显示剩余2条评论

-1

在elem的.current上设置一个useEffect:

  let elem = useRef();
  
  useEffect(() => {
    // ...
  }, [elem.current]);


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接