componentDidMount вызывается ПЕРЕД обратным вызовом ref

86

Проблема

Я устанавливаю реакцию, refиспользуя определение встроенной функции

render = () => {
    return (
        <div className="drawer" ref={drawer => this.drawerRef = drawer}>

то в componentDidMountDOM ссылка не установлена

componentDidMount = () => {
    // this.drawerRef is not defined

Насколько я понимаю, refобратный вызов должен выполняться во время монтирования, однако добавление console.logвыражений componentDidMountвызывается перед функцией обратного вызова ref.

Другие образцы кода, на которые я смотрел, например, это обсуждение на github указывает на то же предположение, его componentDidMountследует вызывать после любых refобратных вызовов, определенных в render, это даже указано в разговоре

Итак, componentDidMount запускается после выполнения всех обратных вызовов ref?

Да.

Я использую реакцию 15.4.1

Что-то еще я пробовал

Чтобы убедиться, что refфункция вызывается, я попытался определить ее в классе как таковой

setDrawerRef = (drawer) => {
  this.drawerRef = drawer;
}

затем в render

<div className="drawer" ref={this.setDrawerRef}>

Ведение журнала консоли в этом случае показывает, что обратный вызов действительно вызывается после componentDidMount

quickshiftin
источник
6
Я могу ошибаться, но когда вы используете стрелочную функцию для методов рендеринга, она будет захватывать значение thisиз лексической области за пределами вашего класса. Попробуйте избавиться от синтаксиса стрелочной функции для методов вашего класса и посмотрите, поможет ли это.
Йоши
3
@GProst В этом суть моего вопроса. Я помещаю console.log в обе функции, и сначала выполняется componentDidMount, а затем - обратный вызов ref.
quickshiftin
3
Просто была аналогичная проблема - для нас, в основном, мы упустили ее с самого начала renderи, следовательно, должны были использовать ее componentDidUpdate, поскольку componentDidMountне является частью жизненного цикла обновления . Вероятно, это не ваша проблема, но подумал, что ее стоит поднять как потенциальное решение.
Alexander Nied
4
То же самое и с React 16. В документации четко указано, ref callbacks are invoked before componentDidMount or componentDidUpdate lifecycle hooks.но это не похоже на правду :(
Райан Х.
1
1. Объявление стрелки ref: ref = {ref => { this.drawerRef = ref }}2. даже ссылки вызываются перед componentDidMount; ref можно получить только после первоначального рендеринга при рендеринге div в вашем случае. Таким образом, вы должны иметь возможность получить доступ к ссылке на следующем уровне, т.е. в componentWillReceiveProps, используя this.drawerRef3. Если вы попытаетесь получить доступ до первоначального монтирования, вы получите только неопределенные значения ref.
bh4r4th 04

Ответы:

154

Короткий ответ:

React гарантирует, что ссылки установлены раньше componentDidMountили componentDidUpdateхуки. Но только для детей, которые действительно были обработаны .

componentDidMount() {
  // can use any refs here
}

componentDidUpdate() {
  // can use any refs here
}

render() {
  // as long as those refs were rendered!
  return <div ref={/* ... */} />;
}

Обратите внимание, это не означает, что «React всегда устанавливает все ссылки перед запуском этих хуков».
Давайте посмотрим на несколько примеров, когда ссылки не устанавливаются.


Ссылки не устанавливаются для элементов, которые не были отображены

React будет вызывать обратные вызовы ref только для элементов, которые вы фактически вернули из рендеринга .

Это означает, что если ваш код выглядит как

render() {
  if (this.state.isLoading) {
    return <h1>Loading</h1>;
  }

  return <div ref={this._setRef} />;
}

и первоначально this.state.isLoadingэто true, вы должны не ожидать , this._setRefчтобы быть вызвана перед componentDidMount.

Это должно иметь смысл: если ваш первый рендеринг вернулся <h1>Loading</h1>, у React нет возможности узнать, что при каком-то другом условии он возвращает что-то еще, к чему нужно прикрепить ссылку. Там нет также ничего установить реф на:<div> элемент не был создан , так как render()метод сказал , что это не должно быть оказано.

Итак, в этом примере componentDidMountбудет срабатывать только . Однако при this.state.loadingизменении наfalse вы this._setRefсначала увидите прикрепленное, а затем componentDidUpdateсработаете.


Остерегайтесь других компонентов

Обратите внимание: если вы передадите дочерние элементы с refs другим компонентам, есть вероятность, что они делают что-то, что предотвращает рендеринг (и вызывает проблему).

Например, это:

<MyPanel>
  <div ref={this.setRef} />
</MyPanel>

не будет работать, если MyPanelне включить props.childrenв его вывод:

function MyPanel(props) {
  // ignore props.children
  return <h1>Oops, no refs for you today!</h1>;
}

Опять же, это не ошибка: React не на что будет устанавливать ссылку, потому что элемент DOM не был создан .


Ссылки не устанавливаются перед жизненными циклами, если они передаются вложенному ReactDOM.render()

Подобно предыдущему разделу, если вы передадите дочерний элемент со ссылкой другому компоненту, возможно, что этот компонент может сделать что-то, что не позволяет вовремя прикрепить ссылку.

Например, возможно, он не возвращает потомка render(), а вместо этого вызывает ReactDOM.render()ловушку жизненного цикла. Вы можете найти пример здесь . В этом примере мы визуализируем:

<MyModal>
  <div ref={this.setRef} />
</MyModal>

Но MyModalвыполняет ReactDOM.render()вызов в своем componentDidUpdate методе жизненного цикла:

componentDidUpdate() {
  ReactDOM.render(this.props.children, this.targetEl);
}

render() {
  return null;
}

Начиная с React 16, такие вызовы рендеринга верхнего уровня во время жизненного цикла будут отложены до тех пор, пока жизненные циклы не будут выполнены для всего дерева . Это объясняет, почему вы не видите прикрепленных вовремя ссылок.

Решение этой проблемы - использовать порталы вместо вложенных ReactDOM.renderвызовов:

render() {
  return ReactDOM.createPortal(this.props.children, this.targetEl);
}

Таким образом, наш <div>с ref фактически включается в вывод рендеринга.

Поэтому, если вы столкнулись с этой проблемой, вам необходимо убедиться, что между вашим компонентом и ссылкой нет ничего, что могло бы задержать рендеринг дочерних элементов.

Не использовать setStateдля хранения ссылок

Убедитесь, что вы не используете setStateдля сохранения ref в обратном вызове ref, так как он асинхронный и до того, как он "завершится", componentDidMountбудет выполнен первым.


Все еще проблема?

Если ни один из приведенных выше советов не помог, сообщите о проблеме в React, и мы рассмотрим.

Дан Абрамов
источник
2
Я тоже внес правку в свой ответ, чтобы объяснить эту ситуацию. См. Первый раздел. Надеюсь это поможет!
Дэн Абрамов
Привет, @DanAbramov, спасибо за это! К сожалению, я не смог разработать воспроизводимый случай, когда впервые столкнулся с ним. К сожалению, я больше не работаю над этим проектом и с тех пор не могу воспроизвести его. Однако этот вопрос стал достаточно популярным, и я согласен, попытка найти воспроизводимый случай является ключевой, поскольку многие люди, похоже, сталкиваются с этой проблемой.
quickshiftin
Я думаю, что во многих случаях это было вызвано недоразумением. В React 15 это также могло произойти из-за ошибки, которая была проглочена (React 16 лучше обрабатывает ошибки и предотвращает это). Я буду рад рассмотреть больше случаев, когда это произойдет, поэтому не стесняйтесь добавлять их в комментарии.
Дэн Абрамов
Помогает! Я особо не заметил, что прелоадер есть.
Назарий
1
Этот ответ мне очень помог. Я боролся с какой-то пустой ссылкой на «refs», и оказалось, что «элементы» вообще не рендерились.
MarkSkayff
1

Другое наблюдение за проблемой.

Я понял, что проблема возникла только в режиме разработки. После дополнительного исследования я обнаружил, что отключение react-hot-loaderв моей конфигурации Webpack предотвращает эту проблему.

Я использую

  • "react-hot-loader": "3.1.3"
  • "webpack": "4.10.2",

И это электронное приложение.

Моя частичная конфигурация разработки Webpack

const webpack = require('webpack')
const merge = require('webpack-merge')
const baseConfig = require('./webpack.config.base')

module.exports = merge(baseConfig, {

  entry: [
    // REMOVED THIS -> 'react-hot-loader/patch',
    `webpack-hot-middleware/client?path=http://localhost:${port}/__webpack_hmr`,
    '@babel/polyfill',
    './app/index'
  ],
  ...
})

Это стало подозрительным, когда я увидел, что использование встроенной функции в render () работает, но использование связанного метода дает сбой.

Работает в любом случае

class MyComponent {
  render () {
    return (
      <input ref={(el) => {this.inputField = el}}/>
    )
  }
}

Сбой при использовании response-hot-loader (ссылка не определена в componentDidMount)

class MyComponent {
  constructor (props) {
    super(props)
    this.inputRef = this.inputRef.bind(this)
  }

  inputRef (input) {
    this.inputField = input
  }

  render () {
    return (
      <input ref={this.inputRef}/>
    )
  }
}

Честно говоря, горячую перезагрузку часто бывает проблематично получить "правильно". Благодаря быстрому обновлению инструментов разработки каждый проект имеет свою конфигурацию. Возможно, мою конкретную конфигурацию можно исправить. Я дам вам знать здесь, если это так.

Кев
источник
Это могло бы объяснить, почему у меня проблемы с этим в CodePen, но в моем случае использование встроенной функции не помогло.
robartsd,
0

Проблема также может возникнуть, когда вы пытаетесь использовать ссылку на отключенный компонент, например, используя ссылку в setinterval, и не очищаете установленный интервал во время размонтирования компонента.

componentDidMount(){
    interval_holder = setInterval(() => {
    this.myref = "something";//accessing ref of a component
    }, 2000);
  }

всегда очищать интервал, например,

componentWillUnmount(){
    clearInterval(interval_holder)
}
случайный кодировщик
источник