Реагировать «после рендеринга» кода?


367

У меня есть приложение, в котором мне нужно динамически установить высоту элемента (скажем, «app-content»). Он берет высоту "хрома" приложения и вычитает его, а затем устанавливает высоту "контента приложения", чтобы он соответствовал 100% в рамках этих ограничений. Это очень просто с ванильными представлениями JS, jQuery или Backbone, но я изо всех сил пытаюсь выяснить, каким будет правильный процесс для этого в React?

Ниже приведен пример компонента. Я хочу иметь возможность установить app-contentвысоту 100% окна минус размер ActionBarи BalanceBar, но как я узнаю, когда все отрисовано и куда я помещу расчетные вещи в этот класс React?

/** @jsx React.DOM */
var List = require('../list');
var ActionBar = require('../action-bar');
var BalanceBar = require('../balance-bar');
var Sidebar = require('../sidebar');
var AppBase = React.createClass({
  render: function () {
    return (
      <div className="wrapper">
        <Sidebar />
        <div className="inner-wrapper">
          <ActionBar title="Title Here" />
          <BalanceBar balance={balance} />
          <div className="app-content">
            <List items={items} />
          </div>
        </div>
      </div>
    );
  }
});

module.exports = AppBase;

4
Сначала я подумал: «Как бы Flexbox это исправить?» потом я вспомнил функцию колонки во Flexbox, и она работала как шарм!
Оскар Годсон

Ответы:


301

componentDidMount ()

Этот метод вызывается один раз после визуализации вашего компонента. Так что ваш код будет выглядеть так.

var AppBase = React.createClass({
  componentDidMount: function() {
    var $this = $(ReactDOM.findDOMNode(this));
    // set el height and width etc.
  },

  render: function () {
    return (
      <div className="wrapper">
        <Sidebar />
          <div className="inner-wrapper">
            <ActionBar title="Title Here" />
            <BalanceBar balance={balance} />
            <div className="app-content">
              <List items={items} />
          </div>
        </div>
      </div>
    );
  }
});

214
или componentDidUpdateесли значения могут измениться после первого рендера.
zackify

5
Я пытаюсь изменить свойство CSS, которое установлено на переход, чтобы анимация начиналась после рендеринга. К сожалению, изменение css в componentDidMount()не вызывает переход.
eye_mew

8
Спасибо. Имя настолько интуитивно понятно, что я удивляюсь, почему я пробовал смешные имена, такие как «init» или даже «initialize».
Павел

13
Изменение его в componentDidMount слишком быстро для браузера. Оберните его в setTimeout и не указывайте фактическое время. то есть componentDidMount: () => { setTimeout(addClassFunction())}, или используйте rAF, ответ ниже этого обеспечивает этот ответ.

3
Это, безусловно, не работает. Если вы получите список узлов, а затем попытаетесь перебрать список узлов, вы обнаружите, что длина равна 0. Выполнение setTimeout и ожидание в течение 1 секунды работали для меня. К сожалению, не похоже, что у реакции есть метод, который действительно ждет, пока не будет обработан DOM.
NickJ

241

Один недостаток использования componentDidUpdate, илиcomponentDidMount является то, что они на самом деле выполняются до того, как элементы dom завершены, но после того, как они были переданы из React в DOM браузера.

Скажем, например, если вам нужно было установить node.scrollHeight для визуализированного node.scrollTop, то элементов DOM React может быть недостаточно. Вам нужно подождать, пока элементы будут окрашены, чтобы получить их высоту.

Решение:

Используйте, requestAnimationFrameчтобы гарантировать, что ваш код запускается после рисования вашего недавно отрендеренного объекта

scrollElement: function() {
  // Store a 'this' ref, and
  var _this = this;
  // wait for a paint before running scrollHeight dependent code.
  window.requestAnimationFrame(function() {
    var node = _this.getDOMNode();
    if (node !== undefined) {
      node.scrollTop = node.scrollHeight;
    }
  });
},
componentDidMount: function() {
  this.scrollElement();
},
// and or
componentDidUpdate: function() {
  this.scrollElement();
},
// and or
render: function() {
  this.scrollElement()
  return [...]

29
window.requestAnimationFrame было недостаточно для меня. Мне пришлось взломать его с помощью window.setTimeout. Argggghhhhhh !!!!!
Алекс

2
Странный. Возможно, это изменилось в самой последней версии React, я не думаю, что вызов requestAnimationFrame необходим. Документация гласит: «Вызывается сразу после того, как обновления компонента сброшены в DOM. Этот метод не вызывается для начального рендеринга. Используйте это как возможность работать с DOM, когда компонент был обновлен». ... т.е. он очищен, узел DOM должен присутствовать. - facebook.github.io/react/docs/…
Джим Сохо

1
@JimSoho, я надеюсь, что вы правы, что это было исправлено, но в этой документации нет ничего нового. Это для крайних случаев, когда обновление dom недостаточно, и важно дождаться цикла рисования. Я пытался создать скрипку с новыми версиями и старыми, но я не мог создать достаточно сложный компонент, чтобы продемонстрировать проблему, даже возвращаясь к нескольким версиям ...
Грэм P Heath

3
@neptunian Строго говоря, «[RAF] вызывается [...] до следующей перекраски ...» - [ developer.mozilla.org/en-US/Apps/Fundamentals/Performance/… ]. В этой ситуации узлу по-прежнему необходимо рассчитывать макет DOM (он же «перекомпонован»). Это использует RAF как способ перехода от макета к макету после. Документация по браузеру от Elm - хорошее место для большего: elmprogramming.com/virtual-dom.html#how-browsers-render-html
Грэм П Хит

1
_this.getDOMNode is not a functionкакого черта этот код?
Оззи

104

По моему опыту, этого window.requestAnimationFrameбыло недостаточно, чтобы убедиться, что DOM был полностью обработан componentDidMount. У меня работает код, который обращается к DOM сразу после componentDidMountвызова, и использование только window.requestAnimationFrameприведет к тому, что элемент будет присутствовать в DOM; однако обновления размеров элемента еще не отражены, поскольку перекомпоновка еще не произошла.

Единственный действительно надежный способ для этого - заключить мой метод в a setTimeoutи a, window.requestAnimationFrameчтобы гарантировать очистку текущего стека вызовов React перед регистрацией для рендеринга следующего кадра.

function onNextFrame(callback) {
    setTimeout(function () {
        requestAnimationFrame(callback)
    })
}

Если бы мне пришлось размышлять о том, почему это происходит / необходимо, я мог бы увидеть, как React пакетирует обновления DOM и фактически не применяет изменения к DOM до тех пор, пока текущий стек не будет завершен.

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


1
Вам нужен только setTimeout ИЛИ requestAnimationFrame, а не оба.

7
Как правило, вы правы. Тем не менее, в контексте метода React componentDidMount, если вы присоедините requestAnimationFrame до того, как этот стек будет завершен, DOM на самом деле может не обновляться полностью. У меня есть код, который последовательно воспроизводит это поведение в контексте обратных вызовов React. Единственный способ убедиться, что ваш код выполняется (еще раз, в этом конкретном случае использования React) после обновления DOM, - это сначала очистить стек вызовов с помощью setTimeout.
Эллиот Чонг

6
Вы заметите другие комментарии выше, в которых упоминается необходимость такого же обходного пути, например: stackoverflow.com/questions/26556436/react-after-render-code/… Это единственный 100% надежный метод для этого варианта использования React. Если бы мне пришлось рискнуть предположить, это может быть связано с самими пакетными обновлениями React, которые потенциально не могут быть применены в текущем стеке (следовательно, отложив requestAnimationFrame до следующего кадра, чтобы гарантировать применение пакета).
Эллиот Чонг

4
Я думаю, что вам, возможно, придется освежить свои внутренности JS ... altitudelabs.com/blog/what-is-the-javascript-event-loop stackoverflow.com/questions/8058612/…
Эллиот Чонг

2
Ожидание очистки текущего стека вызовов, очевидно, не является «бессмысленным» способом говорить о цикле событий, но, я думаю, каждому из них свой…
Эллиот Чонг

17

Просто чтобы немного обновить этот вопрос новыми методами Hook, вы можете просто использовать useEffectловушку:

import React, { useEffect } from 'react'

export default function App(props) {

     useEffect(() => {
         // your post layout code (or 'effect') here.
         ...
     },
     // array of variables that can trigger an update if they change. Pass an
     // an empty array if you just want to run it once after component mounted. 
     [])
}

Также, если вы хотите запустить перед раскраской раскраски, используйте useLayoutEffectкрючок:

import React, { useLayoutEffect } from 'react'

export default function App(props) {

     useLayoutEffect(() => {
         // your pre layout code (or 'effect') here.
         ...
     }, [])
}

В соответствии с документацией React, useLayoutEffect происходит после всех мутаций DOM Reactionjs.org/docs/hooks-reference.html#uselayouteffect
Филипп Хеберт

Правда, но он запускается до того, как макет получит возможность рисовать Updates scheduled inside useLayoutEffect will be flushed synchronously, before the browser has a chance to paint..
П Фастер

Вы случайно не знаете, запускается ли useEffect после перекомпоновки браузера (а не то, что React называет «рисованием»)? Безопасно ли запрашивать элемент scrollHeight с помощью useEffect?
eMontielG

Это безопасно для использованияEffect
P Fuster

13

Вы можете изменить состояние и затем выполнить свои вычисления в обратном вызове setState . Согласно документации React, это «гарантированно сработает после применения обновления».

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

Это хорошая альтернатива, window.requestAnimationFrameи у нее нет проблем, о которых упоминали некоторые пользователи (необходимость комбинировать ее setTimeoutили вызывать несколько раз). Например:

class AppBase extends React.Component {
    state = {
        showInProcess: false,
        size: null
    };

    componentDidMount() {
        this.setState({ showInProcess: true }, () => {
            this.setState({
                showInProcess: false,
                size: this.calculateSize()
            });
        });
    }

    render() {
        const appStyle = this.state.showInProcess ? { visibility: 'hidden' } : null;

        return (
            <div className="wrapper">
                ...
                <div className="app-content" style={appStyle}>
                    <List items={items} />
                </div>
                ...
            </div>
        );
    }
}

1
Это мой любимый ответ. Чистый и хороший идиоматический код React.
phatmann

1
Это отличный ответ! Спасибо!
Брайан Джи Хернг Чонг,

1
Это красиво, добавь комментарий!
bingeScripter

11

Я чувствую, что это решение грязное, но здесь мы идем:

componentDidMount() {
    this.componentDidUpdate()
}

componentDidUpdate() {
    // A whole lotta functions here, fired after every render.
}

Теперь я просто сижу здесь и жду поданных голосов.


5
Вы должны соблюдать жизненный цикл компонента React.
Тубал Мартин

2
@ TúbalMartín я знаю. Если у вас есть лучший способ достичь того же результата, не стесняйтесь поделиться.
Яакко Карху

8
Хм, фигуративное +1 для «сидеть здесь и ждать голосов вниз». Храбрец. ; ^)
Ёрш

14
Скорее вызовите метод из обоих жизненных циклов, тогда вам не нужно запускать циклы из других циклов.
Tjorriemorrie

1
componentWillReceiveProps должен сделать это
Пабло

8

У React есть несколько методов жизненного цикла, которые помогают в этих ситуациях, списки, включая, но не ограничиваясь, getInitialState, getDefaultProps, componentWillMount, componentDidMount и т. Д.

В вашем случае и в случаях, когда необходимо взаимодействовать с элементами DOM, вам нужно подождать, пока dom не будет готов, поэтому используйте componentDidMount, как показано ниже:

/** @jsx React.DOM */
var List = require('../list');
var ActionBar = require('../action-bar');
var BalanceBar = require('../balance-bar');
var Sidebar = require('../sidebar');
var AppBase = React.createClass({
  componentDidMount: function() {
    ReactDOM.findDOMNode(this).height = /* whatever HEIGHT */;
  },
  render: function () {
    return (
      <div className="wrapper">
        <Sidebar />
        <div className="inner-wrapper">
          <ActionBar title="Title Here" />
          <BalanceBar balance={balance} />
          <div className="app-content">
            <List items={items} />
          </div>
        </div>
      </div>
    );
  }
});

module.exports = AppBase;

Также для получения дополнительной информации о жизненном цикле реакции вы можете посмотреть следующую ссылку: https://facebook.github.io/react/docs/state-and-lifecycle.html.

getInitialState, getDefaultProps, componentWillMount, componentDidMount


Мой компонент монтировал запуски до того, как страница рендеринга вызывает большую задержку, так как вызов API загружает данные.
Джейсон Г

6

Я столкнулся с той же проблемой.

В большинстве сценариев используется взлом setTimeout(() => { }, 0)в componentDidMount()работало.

Но не в особом случае; и я не хотел использовать, ReachDOM findDOMNodeтак как документация говорит:

Примечание: findDOMNode - это аварийный штрих, используемый для доступа к базовому узлу DOM. В большинстве случаев использование этого аварийного люка не рекомендуется, поскольку оно пробивает абстракцию компонента.

(Источник: findDOMNode )

Таким образом, в этом конкретном компоненте мне пришлось использовать componentDidUpdate()событие, поэтому мой код оказался таким:

componentDidMount() {
    // feel this a little hacky? check this: http://stackoverflow.com/questions/26556436/react-after-render-code
    setTimeout(() => {
       window.addEventListener("resize", this.updateDimensions.bind(this));
       this.updateDimensions();
    }, 0);
}

А потом:

componentDidUpdate() {
    this.updateDimensions();
}

Наконец, в моем случае мне пришлось удалить слушателя, созданного в componentDidMount:

componentWillUnmount() {
    window.removeEventListener("resize", this.updateDimensions.bind(this));
}

2

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

render: function () {
    var style1 = {height: '100px'};
    var style2 = { height: '100px'};

   //window. height actually will get the height of the window.
   var hght = $(window).height();
   var style3 = {hght - (style1 + style2)} ;

    return (
      <div className="wrapper">
        <Sidebar />
        <div className="inner-wrapper">
          <ActionBar style={style1} title="Title Here" />
          <BalanceBar style={style2} balance={balance} />
          <div className="app-content" style={style3}>
            <List items={items} />
          </div>
        </div>
      </div>
    );`
  }

или вы можете указать высоту каждого реагирующего компонента, используя sass. Укажите первые 2 основных компонента div реагирования с фиксированной шириной, а затем высоту основного div третьего компонента с помощью auto. Таким образом, на основе содержимого третьего div будет назначена высота.


2

На самом деле у меня проблемы с похожим поведением, я рендерил элемент видео в компоненте с его атрибутом id, поэтому, когда RenderDOM.render () завершает работу, он загружает плагин, которому нужен идентификатор, чтобы найти заполнитель, и он не может его найти.

SetTimeout с 0 мс внутри componentDidMount () исправил это :)

componentDidMount() {
    if (this.props.onDidMount instanceof Function) {
        setTimeout(() => {
            this.props.onDidMount();
        }, 0);
    }
}

1

Из документации ReactDOM.render () :

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


9
Можете ли вы добавить пример того, как использовать это? Я в основном возвращаю элементы из метода рендеринга, я не вызываю рендер и не предоставляю значения.
августа

23
К сожалению, упомянутый вами обратный вызов доступен только для верхнего уровняReactDOM.render , а не для уровня компонентаReactElement.render (который здесь обсуждается).
Брамус

1
Пример здесь будет полезен
DanV

1
Я нажал на ссылку в вашем ответе и не смог найти строку, которую вы цитировали, и в вашем ответе недостаточно информации, чтобы работать без нее. Пожалуйста, обратитесь к stackoverflow.com/help/how-to-answer за советом о том, как написать хороший вопрос
Benubird

1

Для меня нет комбинации window.requestAnimationFrameилиsetTimeout выдающиеся результаты не дали. Иногда это срабатывало, но не всегда, а иногда было бы слишком поздно.

Я исправил это, повторяя window.requestAnimationFrameстолько раз, сколько необходимо.
(Обычно 0 или 2-3 раза)

Ключ diff > 0: здесь мы можем точно знать, когда страница обновляется.

// Ensure new image was loaded before scrolling
if (oldH > 0 && images.length > prevState.images.length) {
    (function scroll() {
        const newH = ref.scrollHeight;
        const diff = newH - oldH;

        if (diff > 0) {
            const newPos = top + diff;
            window.scrollTo(0, newPos);
        } else {
            window.requestAnimationFrame(scroll);
        }
    }());
}

0

У меня была странная ситуация, когда мне нужно напечатать реагирующий компонент, который получает большой объем данных и рисует на холсте. Я перепробовал все упомянутые подходы, ни один из них не работал надежно для меня, с requestAnimationFrame внутри setTimeout я получаю пустой холст в 20% случаев, поэтому я сделал следующее:

nRequest = n => range(0,n).reduce(
(acc,val) => () => requestAnimationFrame(acc), () => requestAnimationFrame(this.save)
);

По сути, я создал цепочку из requestAnimationFrame, не уверен, что это хорошая идея или нет, но пока это работает для меня в 100% случаев (я использую 30 в качестве значения для n-переменной).


0

На самом деле существует намного более простая и чистая версия, чем использование анимации запроса или тайм-аута. Я удивился, что никто не поднял это: обработчик загрузки vanilla-js. Если вы можете использовать компонент mount, если нет, просто привяжите функцию к обработчику загрузки компонента jsx. Если вы хотите, чтобы функция запускала каждый рендер, также запустите его, прежде чем возвращать результаты в функцию рендеринга. код будет выглядеть так:

runAfterRender = () => 
{
  const myElem = document.getElementById("myElem")
  if(myElem)
  {
    //do important stuff
  }
}

render()
{
  this.runAfterRender()
  return (
    <div
      onLoad = {this.runAfterRender}
    >
      //more stuff
    </div>
  )
}

}


-1

Немного обновления с ES6классами вместоReact.createClass

import React, { Component } from 'react';

class SomeComponent extends Component {
  constructor(props) {
    super(props);
    // this code might be called when there is no element avaliable in `document` yet (eg. initial render)
  }

  componentDidMount() {
    // this code will be always called when component is mounted in browser DOM ('after render')
  }

  render() {
    return (
      <div className="component">
        Some Content
      </div>
    );
  }
}

Также - проверьте методы жизненного цикла компонента React: Жизненный цикл компонента

У каждого компонента есть много методов, подобных componentDidMountнапример.

  • componentWillUnmount() - компонент собирается удалить из браузера DOM

1
Неуважение, но как это отвечает на вопрос? Отображение обновления на ES6 на самом деле не связано с вопросом / ничего не меняет. Все более старые ответы уже говорят о том, как componentDidMount не работает сам по себе.
dave4jr
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.