Code nach dem Rendern reagieren?

367

Ich habe eine App, in der ich die Höhe eines Elements (sagen wir "App-Inhalt") dynamisch einstellen muss. Es nimmt die Höhe des "Chroms" der App und subtrahiert sie und legt dann die Höhe des "App-Inhalts" so fest, dass sie zu 100% in diese Einschränkungen passt. Dies ist mit Vanilla JS-, jQuery- oder Backbone-Ansichten sehr einfach, aber ich habe Schwierigkeiten herauszufinden, wie der richtige Prozess dafür in React aussehen würde.

Unten finden Sie eine Beispielkomponente. Ich möchte in der Lage sein, die app-contentHöhe des Fensters auf 100% des Fensters abzüglich der Größe des ActionBarund einzustellen BalanceBar, aber woher weiß ich, wann alles gerendert wird und wo ich das Berechnungsmaterial in diese Reaktionsklasse einfügen würde?

/** @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 Patensohn
quelle
4
Zuerst dachte ich: "Wie würde Flexbox das beheben?" dann erinnerte ich mich an die Spaltenfunktion in Flexbox und sie funktionierte wie ein Zauber!
Oscar Godson

Antworten:

301

componentDidMount ()

Diese Methode wird einmal aufgerufen, nachdem Ihre Komponente gerendert wurde. Ihr Code würde also so aussehen.

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
quelle
214
oder componentDidUpdatewenn sich die Werte nach dem ersten Rendern ändern können.
Zackify
5
Ich versuche, eine CSS-Eigenschaft zu ändern, die auf Übergang eingestellt ist, sodass die Animation nach dem Rendern beginnt. Leider componentDidMount()bewirkt das Ändern des CSS keinen Übergang.
eye_mew
8
Vielen Dank. Der Name ist so intuitiv, dass ich mich frage, warum ich lächerliche Namen wie "init" oder sogar "initialize" ausprobiert habe.
Pawel
13
Das Ändern in componentDidMount ist für den Browser zu schnell. Wickeln Sie es in ein setTimeout und geben Sie ihm keine tatsächliche Zeit. dh componentDidMount: () => { setTimeout(addClassFunction())}oder verwenden Sie rAF, die Antwort darunter liefert diese Antwort.
3
Dies funktioniert mit Sicherheit NICHT. Wenn Sie eine Knotenliste erhalten und dann versuchen, die Knotenliste zu durchlaufen, werden Sie feststellen, dass die Länge gleich 0 ist. SetTimeout zu tun und 1 Sekunde zu warten, hat für mich funktioniert. Leider scheint React keine Methode zu haben, die wirklich wartet, bis das DOM gerendert ist.
NickJ
241

Ein Nachteil der Verwendung von componentDidUpdateoder componentDidMountbesteht darin, dass sie tatsächlich ausgeführt werden, bevor die dom-Elemente gezeichnet werden, aber nachdem sie von React an das DOM des Browsers übergeben wurden.

Angenommen, Sie müssen node.scrollHeight auf das gerenderte node.scrollTop setzen, dann reichen die DOM-Elemente von React möglicherweise nicht aus. Sie müssen warten, bis die Elemente fertig gestrichen sind, um ihre Höhe zu erhalten.

Lösung:

Verwenden Sie requestAnimationFramediese Option, um sicherzustellen, dass Ihr Code nach dem Malen Ihres neu gerenderten Objekts ausgeführt wird

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
quelle
29
window.requestAnimationFrame hat mir nicht gereicht. Ich musste es mit window.setTimeout hacken. Argggghhhhhh !!!!!
Alex
2
Seltsam. Vielleicht hat es sich in der neuesten Version von React geändert. Ich denke nicht, dass der Aufruf von requestAnimationFrame notwendig ist. In der Dokumentation heißt es: "Wird sofort aufgerufen, nachdem die Aktualisierungen der Komponente in das DOM übertragen wurden. Diese Methode wird beim ersten Rendern nicht aufgerufen. Verwenden Sie diese Option, um das DOM zu bearbeiten, wenn die Komponente aktualisiert wurde." ... dh Wird es geleert, sollte der DOM-Knoten vorhanden sein. - facebook.github.io/react/docs/…
Jim Soho
1
@ JimSoho, ich hoffe du hast Recht, dass dies behoben wurde, aber es gibt eigentlich nichts Neues in dieser Dokumentation. Dies gilt für Randfälle, in denen der zu aktualisierende Dom nicht ausreicht und es wichtig ist, dass wir auf den Malzyklus warten. Ich habe versucht, eine Geige mit den neuen und den alten Versionen zu erstellen, aber ich konnte anscheinend keine Komponente erstellen, die komplex genug ist, um das Problem zu demonstrieren, selbst wenn ich einige Versionen zurückgehe ...
Graham P Heath
3
@neptunian Genau genommen heißt "[RAF] [...] vor dem nächsten Repaint ..." - [ developer.mozilla.org/en-US/Apps/Fundamentals/Performance/… ]. In dieser Situation muss das Layout des Knotens noch vom DOM berechnet werden (auch bekannt als "reflowed"). Dies verwendet RAF, um von vor dem Layout nach nach dem Layout zu springen. Die Browser-Dokumentation von Elm ist ein guter Ort für mehr: elmprogramming.com/virtual-dom.html#how-browsers-render-html
Graham P Heath
1
_this.getDOMNode is not a functionWas zum Teufel ist dieser Code?
OZZIE
104

Nach meiner Erfahrung window.requestAnimationFramewar es nicht genug, um sicherzustellen, dass das DOM vollständig gerendert / reflow-vollständig von war componentDidMount. Ich habe Code ausgeführt, der unmittelbar nach einem componentDidMountAufruf auf das DOM zugreift und dessen alleinige Verwendung dazu führen window.requestAnimationFramewürde, dass das Element im DOM vorhanden ist. Aktualisierungen der Abmessungen des Elements werden jedoch noch nicht berücksichtigt, da noch kein Reflow aufgetreten ist.

Die einzig wirklich zuverlässige Möglichkeit, dies zu erreichen, bestand darin, meine Methode in a setTimeoutund a zu verpacken, window.requestAnimationFrameum sicherzustellen, dass der aktuelle Aufrufstapel von React gelöscht wird, bevor Sie sich für das Rendern des nächsten Frames registrieren.

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

Wenn ich darüber spekulieren müsste, warum dies auftritt / notwendig ist, könnte ich React Batching DOM-Updates sehen und die Änderungen erst nach Abschluss des aktuellen Stacks auf das DOM anwenden.

Wenn Sie DOM-Messungen in dem Code verwenden, den Sie nach den React-Rückrufen auslösen, möchten Sie wahrscheinlich diese Methode verwenden.

Elliot Chong
quelle
1
Sie benötigen nur das setTimeout ODER den requestAnimationFrame, nicht beide.
7
Normalerweise hast du recht. Wenn Sie jedoch im Kontext der componentDidMount-Methode von React einen requestAnimationFrame anhängen, bevor dieser Stapel fertig ist, wird das DOM möglicherweise nicht vollständig aktualisiert. Ich habe Code, der dieses Verhalten im Kontext der Rückrufe von React konsistent reproduziert. Die einzige Möglichkeit, um sicherzustellen, dass Ihr Code ausgeführt wird (erneut in diesem speziellen Anwendungsfall von React), nachdem das DOM aktualisiert wurde, besteht darin, den Aufrufstapel zuerst mit einem setTimeout löschen zu lassen.
Elliot Chong
6
Sie werden andere Kommentare oben bemerken, in denen erwähnt wird, dass dieselbe Problemumgehung erforderlich ist , z. B .: Stackoverflow.com/questions/26556436/react-after-render-code/… Dies ist die einzige 100% zuverlässige Methode für diesen React-Anwendungsfall. Wenn ich eine Vermutung wagen müsste, könnte dies daran liegen, dass React-Batching-Updates selbst durchgeführt werden, die möglicherweise nicht im aktuellen Stapel angewendet werden (wodurch der requestAnimationFrame auf den nächsten Frame verschoben wird, um sicherzustellen, dass der Batch angewendet wird).
Elliot Chong
4
Ich denke, Sie müssen möglicherweise Ihre JS-Interna auffrischen ... altitudelabs.com/blog/what-is-the-javascript-event-loop stackoverflow.com/questions/8058612/…
Elliot Chong
2
Das Warten auf das Löschen des aktuellen Aufrufstapels ist offensichtlich keine "unsinnige" Art, über die Ereignisschleife zu sprechen, aber für jeden seine eigene, denke ich ...
Elliot Chong
17

Um diese Frage mit den neuen Hook-Methoden ein wenig zu aktualisieren, können Sie einfach den useEffectHook verwenden:

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

Auch wenn Sie vor dem Layout malen möchten, verwenden Sie den useLayoutEffectHaken:

import React, { useLayoutEffect } from 'react'

export default function App(props) {

     useLayoutEffect(() => {
         // your pre layout code (or 'effect') here.
         ...
     }, [])
}
P Fuster
quelle
Gemäß der Dokumentation Reaktion, geschieht useLayoutEffect nach all DOM Mutationen reactjs.org/docs/hooks-reference.html#uselayouteffect
Philippe Hebert
Stimmt, aber es wird ausgeführt, bevor das Layout eine Chance zum Malen hat, die Updates scheduled inside useLayoutEffect will be flushed synchronously, before the browser has a chance to paint.ich bearbeiten werde.
P Fuster
Wissen Sie zufällig, ob useEffect nach dem Reflow des Browsers ausgeführt wird (nicht das, was React als "Farbe" bezeichnet)? Ist es sicher, die scrollHeight eines Elements mit useEffect anzufordern?
eMontielG
Es ist sicher für den GebrauchEffect
P Fuster
13

Sie können den Status ändern und dann Ihre Berechnungen im setState-Rückruf durchführen . Laut der React-Dokumentation wird "garantiert ausgelöst, nachdem das Update angewendet wurde".

Dies sollte im componentDidMountoder an einer anderen Stelle im Code (wie bei einem Ereignishandler zur Größenänderung) und nicht im Konstruktor erfolgen.

Dies ist eine gute Alternative zu window.requestAnimationFrameund es gibt nicht die Probleme, die einige Benutzer hier erwähnt haben (sie müssen mit kombiniert setTimeoutoder mehrmals aufgerufen werden). Zum Beispiel:

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
quelle
1
Das ist meine Lieblingsantwort. Sauberer und guter idiomatischer Reaktionscode.
Phatmann
1
Das ist eine großartige Antwort! Vielen Dank!
Bryan Jyh Herng Chong
1
Das ist wunderschön, stimme diesem Kommentar zu!
BingeScripter
11

Ich finde, dass diese Lösung schmutzig ist, aber los geht's:

componentDidMount() {
    this.componentDidUpdate()
}

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

Jetzt sitze ich einfach hier und warte auf die Abstimmungen.

Jaakko Karhu
quelle
5
Sie sollten den Lebenszyklus einer Reaktionskomponente berücksichtigen.
Túbal Martín
2
@ TúbalMartín Ich weiß. Wenn Sie einen besseren Weg haben, um das gleiche Ergebnis zu erzielen, können Sie es gerne teilen.
Jaakko Karhu
8
Ähm, eine bildliche +1 für "Setz dich hierher und warte auf die Abstimmungen". Mutiger Mann. ; ^)
Ruffin
14
Rufen Sie lieber eine Methode aus beiden Lebenszyklen auf, dann müssen Sie keine Zyklen aus anderen Zyklen auslösen.
Tjorriemorrie
1
componentWillReceiveProps sollte dies tun
Pablo
8

React verfügt nur über wenige Lebenszyklusmethoden, die in diesen Situationen hilfreich sind. Die Listen umfassen unter anderem getInitialState, getDefaultProps, componentWillMount, componentDidMount usw.

In Ihrem Fall und in den Fällen, die mit den DOM-Elementen interagieren müssen, müssen Sie warten, bis der Dom bereit ist. Verwenden Sie also componentDidMount wie folgt :

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

Weitere Informationen zum Lebenszyklus in Reaktion finden Sie unter folgendem Link: https://facebook.github.io/react/docs/state-and-lifecycle.html

getInitialState, getDefaultProps, componentWillMount, componentDidMount

Alireza
quelle
Meine Komponente hat Mount-Läufe ausgeführt, bevor die Seite gerendert wurde, was eine große Verzögerung verursachte, da ein API-Aufruf Daten lädt.
Jason G
6

Ich bin auf das gleiche Problem gestoßen.

In den meisten Szenarien mit dem Hack-ish setTimeout(() => { }, 0)in componentDidMount()gearbeitet.

Aber nicht in einem besonderen Fall; und ich wollte das nicht verwenden, ReachDOM findDOMNodeda in der Dokumentation steht:

Hinweis: findDOMNode ist eine Escape-Schraffur, mit der auf den zugrunde liegenden DOM-Knoten zugegriffen wird. In den meisten Fällen wird von der Verwendung dieser Notluke abgeraten, da sie die Komponentenabstraktion durchdringt.

(Quelle: findDOMNode )

In dieser speziellen Komponente musste ich das componentDidUpdate()Ereignis verwenden, sodass mein Code folgendermaßen aussah:

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

Und dann:

componentDidUpdate() {
    this.updateDimensions();
}

Schließlich musste ich in meinem Fall den Listener entfernen, der in erstellt wurde componentDidMount:

componentWillUnmount() {
    window.removeEventListener("resize", this.updateDimensions.bind(this));
}
ron.camaron
quelle
2

Nach dem Rendern können Sie die Höhe wie unten angeben und die Höhe für die entsprechenden Reaktionskomponenten angeben.

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

oder Sie können die Höhe jeder Reaktionskomponente mit sass angeben. Geben Sie die ersten 2 Reaktionskomponenten-Hauptdivs mit fester Breite und dann die Höhe der dritten Komponentenhauptdivs mit Auto an. Basierend auf dem Inhalt des dritten Div wird die Höhe zugewiesen.

prudhvi seeramreddi
quelle
2

Ich habe tatsächlich Probleme mit einem ähnlichen Verhalten. Ich rendere ein Videoelement in einer Komponente mit dem ID-Attribut. Wenn RenderDOM.render () beendet wird, wird ein Plugin geladen, das die ID benötigt, um den Platzhalter zu finden, und es kann ihn nicht finden.

Das setTimeout mit 0ms innerhalb der componentDidMount () hat es behoben :)

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

Aus der Dokumentation zu ReactDOM.render () :

Wenn der optionale Rückruf bereitgestellt wird, wird er ausgeführt, nachdem die Komponente gerendert oder aktualisiert wurde.

Juampy NR
quelle
9
Können Sie ein Beispiel für die Verwendung hinzufügen? Ich gebe meistens Elemente von der Rendermethode zurück, rufe nicht rendern auf und gebe Werte an.
dcsan
23
Leider ist der von Ihnen erwähnte Rückruf nur für die oberste ReactDOM.renderEbene ReactElement.renderverfügbar , nicht für die Komponentenebene (die hier Gegenstand ist).
Bramus
1
Beispiel hier wäre hilfreich
DanV
1
Ich habe in Ihrer Antwort auf den Link geklickt und die von Ihnen zitierte Zeile nicht gefunden. Ihre Antwort enthält nicht genügend Informationen, um ohne sie zu arbeiten. Bitte lesen Sie stackoverflow.com/help/how-to-answer für Ratschläge, wie man eine gute Frage
schreibt
1

Für mich keine Kombination von window.requestAnimationFrameodersetTimeout konsistente Ergebnisse. Manchmal funktionierte es, aber nicht immer - oder manchmal war es zu spät.

Ich habe es behoben, indem ich window.requestAnimationFrameso oft wie nötig eine Schleife gemacht habe .
(Normalerweise 0 oder 2-3 mal)

Der Schlüssel ist diff > 0: Hier können wir genau sicherstellen, wann die Seite aktualisiert wird.

// 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
quelle
0

Ich hatte eine seltsame Situation, als ich eine Reaktionskomponente drucken musste, die große Datenmengen empfängt und auf Leinwand malt. Ich habe alle genannten Ansätze ausprobiert, von denen keiner für mich zuverlässig funktioniert hat. Mit requestAnimationFrame in setTimeout erhalte ich in 20% der Fälle eine leere Leinwand, also habe ich Folgendes getan:

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

Grundsätzlich habe ich eine Kette von requestAnimationFrames erstellt. Ich bin mir nicht sicher, ob dies eine gute Idee ist oder nicht, aber dies funktioniert bisher in 100% der Fälle für mich (ich verwende 30 als Wert für die Variable n).

Anatoly Strashkevich
quelle
0

Es gibt tatsächlich eine viel einfachere und sauberere Version als die Verwendung von Anforderungsanimationsrahmen oder Zeitüberschreitungen. Ich bin überrascht, dass niemand darauf eingegangen ist: der Vanilla-Js-Onload-Handler. Wenn Sie können, verwenden Sie component did mount. Wenn nicht, binden Sie einfach eine Funktion an den Onload-Hanlder der jsx-Komponente. Wenn Sie möchten, dass die Funktion jedes Rendern ausführt, führen Sie sie auch aus, bevor Sie die Ergebnisse in der Renderfunktion zurückgeben. Der Code würde so aussehen:

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

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

}}

Farantir
quelle
-1

Ein bisschen Update mit ES6Klassen stattReact.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>
    );
  }
}

Überprüfen Sie auch die Methoden zum Reagieren des Komponentenlebenszyklus: Der Komponentenlebenszyklus

Jede Komponente hat viele ähnliche Methoden wie componentDidMountz.

  • componentWillUnmount() - Die Komponente wird aus dem Browser-DOM entfernt
pie6k
quelle
1
Keine Respektlosigkeit, aber wie beantwortet dies die Frage? Das Anzeigen eines Updates auf ES6 hat nichts mit der Frage zu tun / ändert nichts. Alle viel älteren Antworten sprechen bereits davon, dass componentDidMount nicht alleine funktioniert.
Dave4JR