Reagować kod „po renderowaniu”?

367

Mam aplikację, w której muszę dynamicznie ustawiać wysokość elementu (powiedzmy „zawartość aplikacji”). Bierze wysokość „chrome” aplikacji i odejmuje ją, a następnie ustawia wysokość „zawartości aplikacji”, aby zmieściła się w 100% w obrębie tych ograniczeń. Jest to bardzo proste w przypadku waniliowych widoków JS, jQuery lub Backbone, ale staram się dowiedzieć, jaki byłby właściwy proces, aby to zrobić w React?

Poniżej znajduje się przykładowy komponent. Chcę móc ustawić app-contentwysokość na 100% okna minus rozmiar ActionBari BalanceBar, ale skąd mam wiedzieć, kiedy wszystko jest renderowane i gdzie powinienem umieścić obliczenia w tej klasie 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;
Oscar Godson
źródło
4
Na początku pomyślałem „jak Flexbox to naprawi?” potem przypomniałem sobie funkcję kolumny w Flexboksie i działała jak urok!
Oscar Godson

Odpowiedzi:

301

componentDidMount ()

Ta metoda jest wywoływana raz po renderowaniu komponentu. Twój kod tak by wyglądał.

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>
    );
  }
});
Harborhoffer
źródło
214
lub componentDidUpdatejeśli wartości mogą ulec zmianie po pierwszym renderowaniu.
zackify
5
Próbuję zmienić właściwość css, która jest ustawiona na przejście, aby animacja rozpoczynała się po renderowaniu. Niestety zmiana css w componentDidMount()nie powoduje przejścia.
eye_mew
8
Dzięki. Nazwa jest tak intuicyjna, że ​​zastanawiam się, dlaczego próbowałem wymyślić śmieszne nazwy, takie jak „init”, a nawet „inicjalizować”.
Paweł
13
Zmiana w module componentDidMount jest zbyt szybka dla przeglądarki. Zawiń go w setTimeout i nie podawaj rzeczywistego czasu. tj. componentDidMount: () => { setTimeout(addClassFunction())}lub użyj rAF, odpowiedź poniżej zapewnia tę odpowiedź.
3
To z pewnością NIE działa. Jeśli otrzymasz listę węzłów, a następnie spróbujesz wykonać iterację po liście węzłów, przekonasz się, że długość jest równa 0. Wykonanie setTimeout i oczekiwanie na 1 sekundę zadziałało dla mnie. Niestety nie wydaje się, że reakcja ma metodę, która naprawdę czeka aż do renderowania DOM.
NickJ
241

Wadą używania componentDidUpdatelub componentDidMountjest to, że są one faktycznie wykonywane przed narysowaniem elementów dom, ale po ich przekazaniu z React do DOM przeglądarki.

Powiedz na przykład, jeśli potrzebujesz ustawić node.scrollHeight na renderowanym node.scrollTop, to elementy DOM Reacta mogą być niewystarczające. Musisz poczekać, aż elementy zostaną pomalowane, aby uzyskać ich wysokość.

Rozwiązanie:

Użyj, requestAnimationFrameaby upewnić się, że kod jest uruchamiany po malowaniu nowo renderowanego obiektu

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 [...]
Graham P. Heath
źródło
29
window.requestAnimationFrame nie było dla mnie wystarczające. Musiałem zhakować go za pomocą window.setTimeout. Argggghhhhhh !!!!!
Alex
2
Dziwny. Być może zmieniło się to w najnowszej wersji React, nie sądzę, aby wywołanie requestAnimationFrame było konieczne. Dokumentacja mówi: „Wywoływany natychmiast po opróżnieniu aktualizacji komponentu do DOM. Ta metoda nie jest wywoływana do początkowego renderowania. Skorzystaj z tego jako okazji do działania na DOM po zaktualizowaniu komponentu.” ... jest opróżniony, węzeł DOM powinien być obecny. - facebook.github.io/react/docs/…
Jim Soho
1
@JimSoho, mam nadzieję, że masz rację, że zostało to naprawione, ale tak naprawdę nie ma nic nowego w tej dokumentacji. Jest to przypadek, w którym aktualizowany dom nie jest wystarczający i ważne jest, abyśmy czekali na cykl malowania. Próbowałem stworzyć skrzypce z nowymi wersjami i starymi, ale nie mogłem stworzyć wystarczająco złożonego komponentu, aby zademonstrować problem, nawet cofając się o kilka wersji ...
Graham P Heath,
3
@neptunian Ściśle mówiąc „[RAF] nazywa się [...] przed kolejnym przemalowaniem ...” - [ developer.mozilla.org/en-US/Apps/Fundamentals/Performance/… ]. W tej sytuacji węzeł nadal musi mieć obliczony układ przez DOM (inaczej „przepełniony”). Wykorzystuje RAF jako sposób przeskakiwania od układu przed układem po układ. Dokumentacja przeglądarki Elm jest dobrym miejscem na więcej: elmprogramming.com/virtual-dom.html#how-browsers-render-html
Graham P Heath
1
_this.getDOMNode is not a functionco to do cholery jest ten kod?
OZZIE
104

Z mojego doświadczenia window.requestAnimationFramenie było wystarczające, aby zapewnić, że DOM został w pełni zrenderowany / przepełniony componentDidMount. Mam uruchomiony kod, który uzyskuje dostęp do DOM natychmiast po componentDidMountwywołaniu i użycie wyłącznie window.requestAnimationFramespowoduje, że element będzie obecny w DOM; jednak aktualizacje wymiarów elementu nie są jeszcze odzwierciedlone, ponieważ nie nastąpiło jeszcze ponowne wycieki.

Jedynym naprawdę niezawodnym sposobem na to było zawinięcie mojej metody w a setTimeoutoraz a, window.requestAnimationFrameaby upewnić się, że bieżący stos wywołań React zostanie wyczyszczony przed rejestracją do renderowania następnej ramki.

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

Gdybym musiał spekulować, dlaczego tak się dzieje / jest to konieczne, mógłbym zobaczyć React wsadowe aktualizacje DOM i faktyczne stosowanie zmian do DOM dopiero po zakończeniu bieżącego stosu.

Ostatecznie, jeśli używasz pomiarów DOM w kodzie uruchamianym po wywołaniach zwrotnych React, prawdopodobnie będziesz chciał skorzystać z tej metody.

Elliot Chong
źródło
1
Potrzebujesz tylko setTimeout LUB requestAnimationFrame, a nie obu.
7
Zazwyczaj masz rację. Jednak w kontekście metody componentDidMount Reacta, jeśli dołączasz requestAnimationFrame przed zakończeniem stosu, DOM może nie być w pełni zaktualizowany. Mam kod, który konsekwentnie odtwarza to zachowanie w kontekście wywołań zwrotnych React. Jedynym sposobem, aby upewnić się, że kod jest wykonywany (ponownie, w tym konkretnym przypadku użycia React) po aktualizacji DOM, jest zezwolenie na wyczyszczenie stosu wywołań za pomocą setTimeout.
Elliot Chong,
6
Zauważysz inne komentarze powyżej, które wspominają o potrzebie tego samego obejścia, np .: stackoverflow.com/questions/26556436/react-after-render-code/… Jest to jedyna w 100% niezawodna metoda dla tego przypadku użycia React. Gdybym musiał zgadywać, może to wynikać z samych aktualizacji wsadowych React, które potencjalnie nie zostaną zastosowane w bieżącym stosie (stąd odroczenie requestAnimationFrame do następnej ramki, aby upewnić się, że partia jest zastosowana).
Elliot Chong,
4
Myślę, że być może będziesz musiał odświeżyć swoje wewnętrzne elementy JS ... altitudelabs.com/blog/what-is-the-javascript-event-loop stackoverflow.com/questions/8058612/...
Elliot Chong
2
Oczekiwanie na wyczyszczenie bieżącego stosu połączeń nie jest oczywiście „nonsensownym” sposobem mówienia o pętli zdarzeń, ale wydaje mi się, że do każdego własnego ...
Elliot Chong
17

Aby zaktualizować nieco to pytanie za pomocą nowych metod haka, możesz po prostu użyć useEffecthaka:

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. 
     [])
}

Również jeśli chcesz uruchomić przed farbą układu, użyj useLayoutEffecthaka:

import React, { useLayoutEffect } from 'react'

export default function App(props) {

     useLayoutEffect(() => {
         // your pre layout code (or 'effect') here.
         ...
     }, [])
}
P Fuster
źródło
Zgodnie z dokumentacją React, useLayoutEffect ma miejsce po wszystkich mutacjach DOM reagjs.org/docs/hooks-reference.html#uselayouteffect
Philippe Hebert
To prawda, ale działa, zanim układ będzie miał szansę na pomalowanie Updates scheduled inside useLayoutEffect will be flushed synchronously, before the browser has a chance to paint..
P Fuster
Czy wiesz, czy useEffect działa po przepełnieniu przeglądarki (a nie to, co React nazywa „malowaniem”)? Czy za pomocą useEffect można bezpiecznie żądać scrollHeight elementu?
eMontielG
Jest bezpieczny w użyciu Efekt
P Fuster
13

Możesz zmienić stan, a następnie wykonać obliczenia w wywołaniu zwrotnym setState . Zgodnie z dokumentacją React jest to „gwarantowane uruchomienie po zastosowaniu aktualizacji”.

Należy to zrobić w componentDidMountkodzie (lub w innym miejscu w kodzie) (jak w programie obsługi zmiany wielkości), a nie w konstruktorze.

Jest to dobra alternatywa window.requestAnimationFramei nie ma problemów, o których wspominali tutaj niektórzy użytkownicy (konieczność łączenia go setTimeoutlub wywoływania go wiele razy). Na przykład:

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>
        );
    }
}
Joseph238
źródło
1
To moja ulubiona odpowiedź. Czysty i dobry idiomatyczny kod React.
phatmann
1
To świetna odpowiedź! Dzięki!
Bryan Jyh Herng Chong,
1
To jest piękne, głosujcie na ten komentarz!
bingeScripter
11

Wydaje mi się, że to rozwiązanie jest brudne, ale proszę bardzo:

componentDidMount() {
    this.componentDidUpdate()
}

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

Teraz po prostu usiądę tutaj i będę czekać na głosy negatywne.

Jaakko Karhu
źródło
5
Należy przestrzegać cyklu życia komponentu React.
Túbal Martín
2
@ TúbalMartín Wiem. Jeśli masz lepszy sposób na osiągnięcie tego samego rezultatu, podziel się.
Jaakko Karhu,
8
Hm, symboliczny +1 dla „usiądź tutaj i poczekaj na głosy negatywne”. Odważny mężczyzna. ; ^)
ruffin
14
Zamiast tego wywołać metodę z obu cykli życia, wtedy nie trzeba uruchamiać cykli z innych cykli.
Tjorriemorrie
1
componentWillReceiveProps powinien to zrobić
Pablo,
8

React ma kilka metod cyklu życia, które pomagają w takich sytuacjach, listy obejmują między innymi getInitialState, getDefaultProps, componentWillMount, componentDidMount itp.

W twoim przypadku i przypadkach, które muszą wchodzić w interakcje z elementami DOM, musisz poczekać, aż dom będzie gotowy, więc użyj componentDidMount jak poniżej:

/** @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;

Aby uzyskać więcej informacji o cyklu życia w reakcji, możesz zajrzeć do poniższego linku: https://facebook.github.io/react/docs/state-and-lifecycle.html

getInitialState, getDefaultProps, componentWillMount, componentDidMount

Alireza
źródło
mój komponent wykonał montowanie przed renderowaniem strony, co spowodowało duże opóźnienie, gdy wywołanie interfejsu API ładuje dane.
Jason G
6

Natrafiłem na ten sam problem.

W większości scenariuszy korzystania z hack-ish setTimeout(() => { }, 0)w componentDidMount()zadziałało.

Ale nie w szczególnym przypadku; i nie chciałem używać, ReachDOM findDOMNodeponieważ dokumentacja mówi:

Uwaga: findDOMNode jest włazem ewakuacyjnym używanym do uzyskania dostępu do bazowego węzła DOM. W większości przypadków nie zaleca się korzystania z tej klapy ewakuacyjnej, ponieważ przebija ona abstrakcję komponentu.

(Źródło: findDOMNode )

Więc w tym konkretnym komponencie musiałem użyć componentDidUpdate()zdarzenia, więc mój kod był taki:

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);
}

I wtedy:

componentDidUpdate() {
    this.updateDimensions();
}

Wreszcie w moim przypadku musiałem usunąć detektor utworzony w componentDidMount:

componentWillUnmount() {
    window.removeEventListener("resize", this.updateDimensions.bind(this));
}
ron.camaron
źródło
2

Po renderowaniu możesz określić wysokość, jak poniżej, i możesz określić wysokość odpowiednich komponentów reagujących.

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>
    );`
  }

lub możesz określić wysokość każdego reagującego składnika za pomocą sass. Określ najpierw 2 główne działające elementy składowe o stałej szerokości, a następnie wysokość trzeciego głównego elementu składowego z auto. Zatem na podstawie zawartości trzeciego diva zostanie przypisana wysokość.

prudhvi seeramreddi
źródło
2

Mam problem z podobnym zachowaniem, renderuję element wideo w komponencie z jego atrybutem id, więc kiedy RenderDOM.render () kończy, ładuje wtyczkę, która potrzebuje identyfikatora, aby znaleźć symbol zastępczy i nie może go znaleźć.

SetTimeout z 0ms wewnątrz componentDidMount () to naprawił :)

componentDidMount() {
    if (this.props.onDidMount instanceof Function) {
        setTimeout(() => {
            this.props.onDidMount();
        }, 0);
    }
}
Barceyken
źródło
1

Z dokumentacji ReactDOM.render () :

Jeśli podano opcjonalne wywołanie zwrotne, zostanie ono wykonane po renderowaniu lub aktualizacji komponentu.

Juampy NR
źródło
9
czy możesz dodać przykład, jak z tego korzystać? Najczęściej zwracam elementy z metody renderowania, nie wywołuję renderowania i nie podam wartości.
dcsan,
23
Niestety zwrotna wspomnieć jest dostępny tylko w dla tej ToplevelReactDOM.render , nie na poziomie składnikaReactElement.render (co jest przedmiotem tutaj).
Bramus,
1
Przykład tutaj byłby pomocny
DanV
1
Kliknąłem link w twojej odpowiedzi i nie mogłem znaleźć cytowanego wiersza, a twoja odpowiedź nie zawiera wystarczającej ilości informacji, aby bez niego pracować. Proszę zobaczyć stackoverflow.com/help/how-to-answer o radę, jak napisać dobre pytanie
Benubird
1

Dla mnie brak połączenia window.requestAnimationFramelubsetTimeout spójne wyniki. Czasami działało, ale nie zawsze - a czasem byłoby za późno.

Naprawiłem to, zapętlając window.requestAnimationFrametyle razy, ile to konieczne.
(Zazwyczaj 0 lub 2-3 razy)

Kluczem jest diff > 0: tutaj możemy zapewnić dokładnie, kiedy strona się aktualizuje.

// 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);
        }
    }());
}
jackcogdill
źródło
0

Miałem dziwną sytuację, kiedy musiałem wydrukować reagujący element, który otrzymuje dużą ilość danych i maluje na płótnie. Wypróbowałem wszystkie wymienione podejścia, żadne z nich nie działało dla mnie niezawodnie, z requestAnimationFrame wewnątrz setTimeout dostaję puste płótno w 20% czasu, więc zrobiłem następujące:

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

Zasadniczo stworzyłem łańcuch requestAnimationFrame, nie jestem pewien, czy to dobry pomysł, czy nie, ale jak dotąd działa to w 100% przypadków (używam 30 jako wartości zmiennej n).

Anatolij Strashkevich
źródło
0

W rzeczywistości jest o wiele prostsza i czystsza wersja niż używanie animacji ramki lub limitów czasu. Jestem zaskoczony, że nikt tego nie poruszył: moduł obsługi obciążenia waniliowego js. Jeśli możesz, skorzystaj z komponentu montowanego, jeśli nie, po prostu powiąż funkcję na hanlderze onload komponentu jsx. Jeśli chcesz, aby funkcja była uruchamiana przy każdym renderowaniu, uruchom ją również przed zwróceniem wyników w funkcji renderowania. kod wyglądałby tak:

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

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

}

Farantir
źródło
-1

Trochę aktualizacji z ES6klasami zamiastReact.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>
    );
  }
}

Również - sprawdź Metody cyklu życia komponentów React: Cykl życia komponentów

Każdy komponent ma wiele metod podobnych do componentDidMountnp.

  • componentWillUnmount() - element ma zostać usunięty z DOM przeglądarki
pie6k
źródło
1
Bez szacunku, ale jak to odpowiada na pytanie? Wyświetlanie aktualizacji na ES6 nie jest tak naprawdę związane z pytaniem / nic nie zmienia. Wszystkie starsze odpowiedzi już mówią o tym, jak componentDidMount nie działa samodzielnie.
dave4jr