Как мы узнаем, когда изменилось текущее значение React ref?

#javascript #reactjs #react-ref

#javascript #reactjs #react-ref

Вопрос:

Обычно с помощью props мы можем написать

 componentDidUpdate(oldProps) {
  if (oldProps.foo !== this.props.foo) {
    console.log('foo prop changed')
  }
}
  

чтобы обнаружить изменения prop.

Но если мы используем 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 включил бы для этого какую-нибудь простую функцию.

Комментарии:

1. В некоторых случаях вам может сойти с рук просто useLayoutEffect убедиться, что ref не равен null.

2. @grabantot Я вижу, useLayoutEffect после того, как React обновил DOM, и поэтому все ссылки должны были быть изменены в этот момент. Хороший совет. Я думаю, что это достойно того, чтобы быть его собственным ответом!

Ответ №1:

Документы 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 используется для предотвращения двойного вызова обратного вызова ref с null и элементом.

Вы можете инициировать повторный рендеринг при изменении, сохранив текущий узел DOM с useState :

 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() ссылками / object.

Вот тестовый пример, который удаляет и повторно добавляет узел при запуске 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 amp;amp; <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>  

Комментарии:

1. Спасибо. Функция ref в React не идеальна. Ссылки намного проще в Vue, например.

2. @Erwol да, вы можете это сделать. Если вам нужно выполнить повторный рендеринг при изменении узла, используйте useState / setState . Если изменение узла не должно вызывать повторный рендеринг, используйте ref или просто переменную экземпляра (в случае классов). Если вы используете ссылки, вы обычно предпочитаете написать что-то вроде this.containerRef.current = currentNode .

3. как насчет переадресации ссылок? я думаю, мы, возможно, все еще сможем использовать React.createRef() ссылки, если примем ссылку извне компонента ( Comp(props, ref) и т.д.). предполагая, что ссылка обновляется при каждом рендеринге; может ли что-то подобное сработать?

4. Отличный четкий ответ!

5. Это был первый раз, когда я увидел useCallback в примере, который действительно имел смысл для меня. Спасибо!

Ответ №2:

componentDidUpdate вызывается при изменении состояния компонента или реквизитов, поэтому оно не обязательно будет вызываться при 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>  

Комментарии:

1. Итак, в примере на основе классов, когда я должен выполнить эту проверку?

2. Подождите, разве это не componentDidUpdate вызывается после каждого render ? Итак, не componentDidUpdate ли это подходящее место для выполнения проверки (даже если componentDidUpdate это было вызвано косвенно изменениями prop или состояния)?

3. @trusktr Да, вы правы, это componentDidUpdate вызывается косвенно после prop или изменения состояния, но ref это изменяемое значение, которое может быть изменено чем угодно, и React не имеет возможности узнать, что ссылка изменилась в этом смысле. В примере класса вы бы использовали комбинацию componentDidMount и componentDidUpdate . Я обновил ответ.

4. «ссылка — это изменяемое значение, которое может быть изменено чем угодно», верно, но аналогично это может сделать что угодно в this.state , однако мы, очевидно, избегаем этого, потому что это не способ изменить состояние. Аналогично, я думаю, было бы (надеюсь) очевидно, что мы не должны произвольно изменять реквизиты или ссылки. Итак, похоже, что если мы позволим изменять только React ref.current (только путем передачи ref в разметку JSX), то наша идея о необходимости отслеживать старое значение кажется единственным способом сделать это. Было бы неплохо, если бы в React было больше возможностей для этого.

5. Со старыми ссылками (ссылки на основе функций) было легко просто setState ввести новую ссылку внутри функций, что вызвало бы реактивность без необходимости отслеживать старые значения вручную. На первый взгляд, это могло бы быть более интуитивно понятным (например, более очевидным, как обрабатывать реактивность). (Однако меня бесит, что каждый вызов функции ВСЕГДА начинался с нулевой ссылки, что было абсолютно ошеломляющим. Они аргументировали это тем, что это было для принудительной очистки, но я думаю, что это вызвало больше проблем, чем защита от неправильного кода конечного пользователя).