Réagir au code «après rendu»?

367

J'ai une application où je dois définir la hauteur d'un élément (disons "contenu de l'application") dynamiquement. Il prend la hauteur du «chrome» de l'application et la soustrait, puis définit la hauteur du «contenu de l'application» pour s'adapter à 100% à ces contraintes. C'est super simple avec les vues vanilla JS, jQuery ou Backbone, mais j'ai du mal à comprendre quel serait le bon processus pour le faire dans React?

Voici un exemple de composant. Je veux pouvoir définir app-contentla hauteur de 100% de la fenêtre moins la taille de ActionBaret BalanceBar, mais comment savoir quand tout est rendu et où dois-je placer les éléments de calcul dans cette classe 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
la source
4
Au début, je me disais "comment Flexbox pourrait-il résoudre ce problème?" puis je me suis souvenu de la fonctionnalité de colonne dans Flexbox et cela a fonctionné comme un charme!
Oscar Godson

Réponses:

301

componentDidMount ()

Cette méthode est appelée une fois après le rendu de votre composant. Votre code ressemblerait donc à cela.

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
la source
214
ou componentDidUpdatesi les valeurs peuvent changer après le premier rendu.
zackify
5
J'essaie de changer une propriété css qui est définie sur transition, afin que l'animation commence après le rendu. Malheureusement, la modification du CSS componentDidMount()ne provoque pas de transition.
eye_mew
8
Merci. Le nom est si intuitif que je me demande pourquoi j'essayais des noms ridicules comme "init" ou même "initialize".
Pawel
13
Le changer dans componentDidMount est trop rapide pour le navigateur. Enveloppez-le dans un setTimeout et ne lui donnez pas de temps réel. c'est-à componentDidMount: () => { setTimeout(addClassFunction())}- dire , ou utilisez rAF, la réponse ci-dessous fournit cette réponse.
3
Cela ne fonctionne certainement PAS. Si vous obtenez une liste de nœuds, puis essayez d'itérer sur la liste de nœuds, vous constaterez que la longueur est égale à 0. Faire setTimeout et attendre 1 seconde a fonctionné pour moi. Malheureusement, il ne semble pas que react ait une méthode qui attend vraiment le rendu du DOM.
NickJ
241

Un inconvénient de l'utilisation componentDidUpdate, ou componentDidMountest qu'ils sont réellement exécutés avant que les éléments dom ne soient dessinés, mais après qu'ils ont été passés de React au DOM du navigateur.

Par exemple, si vous avez besoin de définir node.scrollHeight sur le node.scrollTop rendu, les éléments DOM de React peuvent ne pas suffire. Vous devez attendre que les éléments soient peints pour obtenir leur hauteur.

Solution:

Utilisez requestAnimationFramepour vous assurer que votre code est exécuté après la peinture de votre nouvel objet rendu

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
la source
29
window.requestAnimationFrame ne me suffisait pas. J'ai dû le pirater avec window.setTimeout. Argggghhhhhh !!!!!
Alex
2
Impair. Peut-être que cela a changé dans la version la plus récente de React, je ne pense pas que l'appel à requestAnimationFrame soit nécessaire. La documentation indique: "Appelé immédiatement après le vidage des mises à jour du composant dans le DOM. Cette méthode n'est pas appelée pour le rendu initial. Utilisez-la comme une opportunité d'opérer sur le DOM lorsque le composant a été mis à jour." ... ie, il est vidé, le noeud DOM doit être présent. - facebook.github.io/react/docs/…
Jim Soho
1
@ JimSoho, j'espère que vous avez raison, que cela a été corrigé, mais il n'y a en fait rien de nouveau dans cette documentation. C'est pour les cas de bord où le dom mis à jour ne suffit pas, et il est important d'attendre le cycle de peinture. J'ai essayé de créer un violon avec les nouvelles versions et les anciennes, mais je n'arrivais pas à créer un composant suffisamment complexe pour démontrer le problème, même en remontant quelques versions ...
Graham P Heath
3
@neptunian Strictement parlant "[RAF] est appelé [...] avant le prochain repeint ..." - [ developer.mozilla.org/en-US/Apps/Fundamentals/Performance/… ]. Dans cette situation, le nœud doit toujours avoir sa disposition calculée par le DOM (aka "reflowed"). Cela utilise RAF comme un moyen de sauter d'avant la mise en page à après la mise en page. La documentation du navigateur d'Elm est un bon endroit pour en savoir plus: elmprogramming.com/virtual-dom.html#how-browsers-render-html
Graham P Heath
1
_this.getDOMNode is not a functionqu'est-ce que c'est que ce code?
OZZIE
104

D'après mon expérience window.requestAnimationFrame, cela ne suffisait pas à garantir que le DOM avait été entièrement rendu / refondu à partir de componentDidMount. J'ai du code en cours d'exécution qui accède au DOM immédiatement après un componentDidMountappel et l'utilisation uniquement window.requestAnimationFrameentraînerait la présence de l'élément dans le DOM; cependant, les mises à jour des dimensions de l'élément ne sont pas encore reflétées car aucune refusion n'a encore eu lieu.

Le seul moyen vraiment fiable pour que cela fonctionne était d'encapsuler ma méthode dans a setTimeoutet a window.requestAnimationFramepour s'assurer que la pile d'appels actuelle de React est effacée avant de s'inscrire pour le rendu de la prochaine image.

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

Si je devais spéculer sur la raison pour laquelle cela se produisait / était nécessaire, je pourrais voir React regrouper les mises à jour du DOM et n'appliquer les modifications au DOM qu'après la fin de la pile actuelle.

En fin de compte, si vous utilisez des mesures DOM dans le code que vous tirez après les rappels React, vous voudrez probablement utiliser cette méthode.

Elliot Chong
la source
1
Vous n'avez besoin que de setTimeout OU de requestAnimationFrame, pas des deux.
7
En règle générale, vous avez raison. Cependant, dans le contexte de la méthode componentDidMount de React, si vous attachez un requestAnimationFrame avant la fin de cette pile, le DOM peut ne pas être entièrement mis à jour. J'ai du code qui reproduit systématiquement ce comportement dans le contexte des rappels de React. La seule façon d'être sûr que votre code s'exécute (encore une fois, dans ce cas d'utilisation spécifique de React) après la mise à jour du DOM est de laisser la pile d'appels effacer d'abord avec un setTimeout.
Elliot Chong
6
Vous remarquerez d'autres commentaires ci-dessus qui mentionnent avoir besoin de la même solution de contournement, à savoir: stackoverflow.com/questions/26556436/react-after-render-code/… C'est la seule méthode fiable à 100% pour ce cas d'utilisation de React. Si je devais deviner, cela peut être dû aux mises à jour de lot React elles-mêmes qui ne sont potentiellement pas appliquées dans la pile actuelle (d'où le report de requestAnimationFrame à la trame suivante pour s'assurer que le lot est appliqué).
Elliot Chong
4
Je pense que vous pourriez avoir besoin de rafraîchir vos internes JS ... altitudelabs.com/blog/what-is-the-javascript-event-loop stackoverflow.com/questions/8058612/…
Elliot Chong
2
Attendre que la pile d'appels en cours soit effacée n'est évidemment pas une façon "absurde" de parler de la boucle d'événement, mais à chacun la sienne, je suppose ...
Elliot Chong
17

Juste pour mettre à jour un peu cette question avec les nouvelles méthodes de hook, vous pouvez simplement utiliser le useEffecthook:

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

De plus, si vous souhaitez exécuter avant la peinture de mise en page, utilisez le useLayoutEffectcrochet:

import React, { useLayoutEffect } from 'react'

export default function App(props) {

     useLayoutEffect(() => {
         // your pre layout code (or 'effect') here.
         ...
     }, [])
}
P Fuster
la source
Selon la documentation de React, useLayoutEffect se produit après toutes les mutations DOM reactjs.org/docs/hooks-reference.html#uselayouteffect
Philippe Hebert
Certes, mais il s'exécute avant que la mise en page ait une chance de peindre, Updates scheduled inside useLayoutEffect will be flushed synchronously, before the browser has a chance to paint.je vais l'éditer.
P Fuster
Savez-vous par hasard si useEffect s'exécute après la refusion du navigateur (pas ce que React appelle la «peinture»)? Est-il sûr de demander scrollHeight d'un élément avec useEffect?
eMontielG
Il est sûr pour une utilisation Effet
P Fuster
13

Vous pouvez modifier l'état, puis effectuer vos calculs dans le rappel setState . Selon la documentation de React, cela est "garanti de se déclencher après l'application de la mise à jour".

Cela devrait être fait dans componentDidMountou ailleurs dans le code (comme sur un gestionnaire d'événement de redimensionnement) plutôt que dans le constructeur.

Ceci est une bonne alternative à window.requestAnimationFrameet il n'a pas les problèmes que certains utilisateurs ont mentionnés ici (devant le combiner avec setTimeoutou l'appeler plusieurs fois). Par exemple:

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
la source
1
Ceci est ma réponse préférée. Code React idiomatique propre et bon.
phatmann
1
C'est une excellente réponse! Merci!
Bryan Jyh Herng Chong
1
C'est magnifique, votez pour ce commentaire!
bingeScripter
11

Je pense que cette solution est sale, mais c'est parti:

componentDidMount() {
    this.componentDidUpdate()
}

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

Maintenant, je vais juste m'asseoir ici et attendre les votes négatifs.

Jaakko Karhu
la source
5
Vous devez respecter le cycle de vie d'un composant React.
Túbal Martín
2
@ TúbalMartín je sais. Si vous avez une meilleure façon d'atteindre le même résultat, n'hésitez pas à partager.
Jaakko Karhu
8
Um, un +1 figuratif pour "asseyez-vous ici et attendez les votes négatifs". Homme courageux. ; ^)
ruffin
14
Appelez plutôt une méthode des deux cycles de vie, vous n'avez donc pas à déclencher de cycles à partir d'autres cycles.
Tjorriemorrie
1
componentWillReceiveProps devrait le faire
Pablo
8

React a peu de méthodes de cycle de vie qui aident dans ces situations, les listes comprenant mais sans s'y limiter getInitialState, getDefaultProps, componentWillMount, componentDidMount etc.

Dans votre cas et les cas qui doivent interagir avec les éléments DOM, vous devez attendre que le dom soit prêt, alors utilisez componentDidMount comme ci-dessous:

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

Pour plus d'informations sur le cycle de vie de React, vous pouvez également consulter le lien ci-dessous: https://facebook.github.io/react/docs/state-and-lifecycle.html

getInitialState, getDefaultProps, componentWillMount, componentDidMount

Alireza
la source
mon composant a monté des exécutions avant le rendu de la page, provoquant un gros retard lors du chargement d'un appel api dans les données.
Jason G
6

J'ai rencontré le même problème.

Dans la plupart des scénarios en utilisant le hack-ish setTimeout(() => { }, 0)dans componentDidMount()travaillé.

Mais pas dans un cas particulier; et je ne voulais pas utiliser le ReachDOM findDOMNodedepuis la documentation dit:

Remarque: findDOMNode est une hachure d'échappement utilisée pour accéder au nœud DOM sous-jacent. Dans la plupart des cas, l'utilisation de cette trappe d'échappement est déconseillée car elle perce l'abstraction du composant.

( Paquet source: findDOMNode )

Donc, dans ce composant particulier, j'ai dû utiliser l' componentDidUpdate()événement, donc mon code a fini par ressembler à ceci:

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

Et alors:

componentDidUpdate() {
    this.updateDimensions();
}

Enfin, dans mon cas, j'ai dû supprimer l'écouteur créé dans componentDidMount:

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

Après le rendu, vous pouvez spécifier la hauteur comme ci-dessous et spécifier la hauteur des composants de réaction correspondants.

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

ou vous pouvez spécifier la hauteur de chaque composant React à l'aide de sass. Spécifiez les 2 premiers composants principaux du composant React à largeur fixe, puis la hauteur du troisième composant principal Div avec Auto. Ainsi, en fonction du contenu du troisième div, la hauteur sera attribuée.

prudhvi seeramreddi
la source
2

J'ai en fait un problème avec un comportement similaire, je rend un élément vidéo dans un composant avec son attribut id donc quand RenderDOM.render () se termine, il charge un plugin qui a besoin de l'id pour trouver l'espace réservé et il ne le trouve pas.

Le setTimeout avec 0 ms à l'intérieur du componentDidMount () l'a corrigé :)

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

Dans la documentation ReactDOM.render () :

Si le rappel facultatif est fourni, il sera exécuté après le rendu ou la mise à jour du composant.

Juampy NR
la source
9
pouvez-vous ajouter un exemple de la façon d'utiliser cela? Je retourne principalement des éléments de la méthode render, je n'appelle pas render et je fournis des valeurs.
dcsan
23
Malheureusement , le rappel que vous mentionnez est uniquement disponible dans le pour le premier niveauReactDOM.render , et non pour le son niveau des composantsReactElement.render (qui est le sujet ici).
Bramus
1
Un exemple ici serait utile
DanV
1
J'ai cliqué sur le lien dans votre réponse, et je n'ai pas trouvé la ligne que vous avez citée, et votre réponse ne contient pas suffisamment d'informations pour travailler sans. Veuillez consulter stackoverflow.com/help/how-to-answer pour savoir comment rédiger une bonne question
Benubird
1

Pour moi, aucune combinaison window.requestAnimationFrameou setTimeoutproduit des résultats cohérents. Parfois, cela fonctionnait, mais pas toujours - ou parfois il était trop tard.

Je l'ai corrigé en bouclant window.requestAnimationFrameautant de fois que nécessaire.
(Généralement 0 ou 2-3 fois)

La clé est diff > 0: ici, nous pouvons garantir exactement quand la page sera mise à jour.

// 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
la source
0

J'ai eu une situation étrange lorsque j'ai besoin d'imprimer un composant réactif qui reçoit une grande quantité de données et de la peinture sur toile. J'ai essayé toutes les approches mentionnées, aucune d'entre elles n'a fonctionné de manière fiable pour moi, avec requestAnimationFrame dans setTimeout, je reçois un canevas vide dans 20% du temps, j'ai donc fait ce qui suit:

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

Fondamentalement, j'ai fait une chaîne de requestAnimationFrame, je ne sais pas si c'est une bonne idée ou non, mais cela fonctionne dans 100% des cas pour moi jusqu'à présent (j'utilise 30 comme valeur pour la variable n).

Anatoly Strashkevich
la source
0

Il existe en fait une version beaucoup plus simple et plus propre que l'utilisation du cadre d'animation de demande ou des délais d'attente. Je suis surpris que personne n'en ait parlé: le gestionnaire de charge vanilla-js. Si vous le pouvez, utilisez le composant did mount, sinon, liez simplement une fonction sur le support de chargement du composant jsx. Si vous souhaitez que la fonction exécute chaque rendu, exécutez-la également avant de vous renvoyer les résultats dans la fonction de rendu. le code ressemblerait à ceci:

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

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

}

Farantir
la source
-1

Un peu de mise à jour avec des ES6classes au lieu deReact.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>
    );
  }
}

Consultez également les méthodes de cycle de vie des composants React: le cycle de vie des composants

Chaque composant a beaucoup de méthodes similaires à componentDidMounteg.

  • componentWillUnmount() - le composant est sur le point d'être supprimé du navigateur DOM
pie6k
la source
1
Aucun manque de respect, mais comment cela répond-il à la question? Afficher une mise à jour sur ES6 n'est pas vraiment lié à la question / ne change rien. Toutes les réponses beaucoup plus anciennes parlent déjà du fait que componentDidMount ne fonctionne pas seul.
dave4jr