当React的ref.current值发生变化时,我们如何知道?

74

通常情况下,使用道具,我们可以编写

componentDidUpdate(oldProps) {
  if (oldProps.foo !== this.props.foo) {
    console.log('foo prop changed')
  }
}
为了检测属性变化,但如果我们使用React.createRef(),如何检测引用何时更改为新的组件或DOM元素?React文档并没有真正提到任何内容。例如:
class Foo extends React.Component {
  someRef = React.createRef()

  componentDidUpdate(oldProps) {
    const refChanged = /* What do we put here? */

    if (refChanged) {
      console.log('new ref value:', this.someRef.current)
    }
  }

  render() {
    // ...
  }
}
我们应该自己实现某种旧值功能吗?例如,
class Foo extends React.Component {
  someRef = React.createRef()
  oldRef = {}

  componentDidMount() {
    this.oldRef.current = this.someRef.current
  }

  componentDidUpdate(oldProps) {
    const refChanged = this.oldRef.current !== this.someRef.current

    if (refChanged) {
      console.log('new ref value:', this.someRef.current)

      this.oldRef.current = this.someRef.current
    }
  }

  render() {
    // ...
  }
}

这就是我们应该做的吗?我本以为React会内置一些简单的功能来实现这个。


在某些情况下,您可以仅使用 useLayoutEffect 来确保引用不为 null。 - grabantot
@grabantot 我明白了,useLayoutEffect 在 React 更新 DOM 后执行,因此任何引用必须在那时已更改。好的技巧。我认为这值得成为自己的答案! - trusktr
2个回答

109

React文档建议使用回调引用来检测ref值的变化。

钩子函数

export function Comp() {
  const onRefChange = useCallback(node => {
    if (node === null) { 
      // DOM node referenced by ref has been unmounted
    } else {
      // DOM node referenced by ref has changed and exists
    }
  }, []); // adjust deps

  return <h1 ref={onRefChange}>Hey</h1>;
}

useCallback用于防止回调函数被空值和元素重复调用

你可以使用useState存储当前DOM节点来触发重新渲染

const [domNode, setDomNode] = useState(null);
const onRefChange = useCallback(node => {
  setDomNode(node); // trigger re-render on changes
  // ...
}, []);

类组件

export class FooClass extends React.Component {
  state = { ref: null, ... };

  onRefChange = node => {
    // same as Hooks example, re-render on changes
    this.setState({ ref: node });
  };

  render() {
    return <h1 ref={this.onRefChange}>Hey</h1>;
  }
}

注意: useRef 不会通知 ref 的变化。同时使用 React.createRef() 或对象 ref 也没有用处

以下是一个测试案例,它在触发 onRefChange 回调时删除并重新添加节点:

const Foo = () => {
  const [ref, setRef] = useState(null);
  const [removed, remove] = useState(false);

  useEffect(() => {
    setTimeout(() => remove(true), 3000); // drop after 3 sec
    setTimeout(() => remove(false), 5000); // ... and mount it again
  }, []);

  const onRefChange = useCallback(node => {
    console.log("ref changed to:", node);
    setRef(node); // or change other state to re-render
  }, []);

  return !removed && <h3 ref={onRefChange}>Hello, world</h3>;
}

ReactDOM.render(<Foo />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.10.1/umd/react.production.min.js" integrity="sha256-vMEjoeSlzpWvres5mDlxmSKxx6jAmDNY4zCt712YCI0=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.10.1/umd/react-dom.production.min.js" integrity="sha256-QQt6MpTdAD0DiPLhqhzVyPs1flIdstR4/R7x4GqCvZ4=" crossorigin="anonymous"></script>

<script> var {useState, useEffect, useCallback} = React</script>

<div id="root"></div>


谢谢。在React中,Ref特性并不是理想的。例如,在Vue中,Ref要简单得多。 - trusktr
1
@Erwol 是的,你可以这样做。如果需要重新渲染,当节点发生变化时,请使用useState/setState。如果节点更改不应触发重新渲染,请使用引用或实例变量(在类的情况下)。如果选择引用,则通常会写类似于this.containerRef.current = currentNode的内容。 - ford04
关于 Ref 转发怎么样?我在想,如果我们从组件外部接受 Ref(例如 Comp(props, ref) 等),是否仍然可以使用 React.createRef() 创建的引用呢?假设每次渲染时 Ref 都会刷新,那么这种方法可行吗? - Eliran Malka
1
非常精炼的答案! - Bhargav Shah
2
这是我第一次看到useCallback的例子,它确实让我感到有意义。谢谢! - chromaloop
如果使用TypeScript,代码会类似于const onRefChange = useCallback((node: HTMLElement | null) => { ... }, []) - undefined

3

componentDidUpdate 会在组件状态或属性改变时被调用,因此当 ref 改变时不一定会被调用,因为它可以随意变化。

如果您想检查一个 ref 是否与之前的渲染结果有所改变,您可以保留另一个引用,并将其与真实引用进行比较。

示例

class App extends React.Component {
  prevRef = null;
  ref = React.createRef();
  state = {
    isVisible: true
  };

  componentDidMount() {
    this.prevRef = this.ref.current;

    setTimeout(() => {
      this.setState({ isVisible: false });
    }, 1000);
  }

  componentDidUpdate() {
    if (this.prevRef !== this.ref.current) {
      console.log("ref changed!");
    }

    this.prevRef = this.ref.current;
  }

  render() {
    return this.state.isVisible ? <div ref={this.ref}>Foo</div> : null;
  }
}

ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>

<div id="root"></div>


在基于类的示例中,我什么时候会进行那个检查? - trusktr
等等,componentDidUpdate 不是在每次 render 之后都会被调用吗?那么,即使 componentDidUpdate 是由属性或状态更改间接触发的,难道不应该在其中进行检查吗? - trusktr
1
@trusktr 是的,你说得对,componentDidUpdate 在 prop 或 state 改变后间接调用,但是 ref 是一个可变的值,可以被任何东西改变,React 无法知道 ref 在这个意义上是否发生了变化。在类示例中,您将使用 componentDidMountcomponentDidUpdate 的组合。我更新了答案。 - Tholle
2
“ref” 是一个可变的值,可以被任何东西修改。但同样地,“this.state” 的任何内容也都可以被改变,但我们很明显要避免这样做,因为这不是更改状态的正确方式。同样地,我认为很明显我们不应该随意修改 props 或 refs。所以,如果我们只让 React 修改 “ref.current”(只通过将 ref 传递到 JSX 标记中),那么我们必须跟踪旧值的想法似乎是唯一的方法。如果 React 在这方面有更多的功能就好了。 - trusktr
1
使用旧的引用(基于函数的引用),只需在函数内部使用新引用进行setState,就可以触发反应性,而无需手动跟踪旧值。回想起来,这可能更直观(即更明显如何处理反应性)。 (但是,我讨厌每次调用函数都必须以空引用开头,这绝对令人费解。他们认为这是为了强制清理,但我认为它引起的问题比防范不良最终用户代码更多)。 - trusktr

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