J'ai une application où je dois définir la hauteur d'un élément (disons "app-content") de manière dynamique. Il prend la hauteur du "chrome" de l'application et le soustrait, puis définit la hauteur du "contenu de l'application" pour qu'il corresponde à 100% à ces contraintes. C’est très 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?
Vous trouverez ci-dessous un exemple de composant. Je veux pouvoir définir la hauteur de app-content
à 100% de la fenêtre moins la taille de ActionBar
et BalanceBar
, mais comment savoir quand tout est rendu et où placer le calcul dans cette classe de réaction?
/** @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;
https://facebook.github.io/react/docs/react-component.html#componentdidmount
Cette méthode est appelée une fois après le rendu de votre composant. Donc, votre code ressemblerait tellement.
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>
);
}
});
L'utilisation de componentDidUpdate
ou componentDidMount
présente l'inconvénient d'être réellement exécutés avant le dessin des éléments dom, mais après leur passage de React dans le DOM du navigateur.
Disons par exemple si vous avez besoin de définir node.scrollHeight sur le noeud rendu.sous.TrollTop, les éléments DOM de React risquent de ne pas suffire. Vous devez attendre que les éléments soient peints pour obtenir leur hauteur.
Solution:
Utilisez requestAnimationFrame
pour vous assurer que votre code est exécuté après la peinture de votre nouvel objet restitué.
scrollElement: function() {
//store a this ref, and
var _this = this;
//wait for a Paint to do scrolly stuff
window.requestAnimationFrame(function() {
var node = _this.getDOMNode();
if (node !== undefined) {
//and scroll them!
node.scrollTop = node.scrollHeight;
}
});
},
componentDidMount: function() {
this.scrollElement();
},
// and or
componentDidUpdate: function() {
this.scrollElement();
},
// and or
render: function() {
this.scrollElement()
return [...]
D'après mon expérience, window.requestAnimationFrame
n'était pas suffisant pour garantir que le DOM avait été entièrement restitué/rediffusé-complet à partir de componentDidMount
. J'ai du code en cours d'exécution qui accède au DOM immédiatement après un appel componentDidMount
et en utilisant uniquement window.requestAnimationFrame
, l'élément en question serait présent dans le DOM; Cependant, les mises à jour des dimensions de l'élément ne sont pas encore reflétées, car un reflux n'a pas encore eu lieu.
Le seul moyen véritablement fiable pour que cela fonctionne est d’envelopper ma méthode dans une setTimeout
et un window.requestAnimationFrame
pour s’assurer que la pile d’appels actuelle de React est effacée avant de s’enregistrer pour le rendu de la prochaine image.
function onNextFrame(callback) {
setTimeout(function () {
window.requestAnimationFrame(callback)
}, 0)
}
Si je devais spéculer sur la raison pour laquelle cela se produit/était nécessaire, je pourrais voir React mettre en lot les mises à jour du DOM en batch et ne pas appliquer les modifications au DOM tant que la pile en cours n'est pas terminée.
En fin de compte, si vous utilisez des mesures DOM dans le code que vous activez après les rappels React, vous souhaiterez probablement utiliser cette méthode.
React a peu de méthodes de cycle de vie utiles dans ces situations, notamment les listes getInitialState, getDefaultProps, composantWillMount, composantDidMount, etc.
Dans votre cas et dans les cas qui doivent interagir avec les éléments DOM, vous devez attendre que le dom soit prêt, utilisez donc composantDidMount 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, vous pouvez également consulter le lien ci-dessous: https://facebook.github.io/react/docs/state-and-lifecycle.html }
Je pense que cette solution est sale, mais on y va:
componentDidMount() {
this.componentDidUpdate()
}
componentDidUpdate() {
// A whole lotta functions here, fired after every render.
}
Je vais maintenant rester assis à attendre les votes négatifs.
Après le rendu, vous pouvez spécifier la hauteur comme ci-dessous et les hauteurs des composants réactifs 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 de réaction en utilisant sass. Spécifiez les deux premières composantes principales du composant à réaction avec une largeur fixe, puis la hauteur de la troisième principale du composant avec auto. Donc, en fonction du contenu de la troisième div, la hauteur sera attribuée.
Vous pouvez modifier l'état puis faire vos calculs dans le callback setState . Selon la documentation de React, il est "garanti qu’il se déclenche après l’application de la mise à jour".
Cela devrait être fait dans componentDidMount
ou ailleurs dans le code (comme dans un gestionnaire d'événements de redimensionnement) plutôt que dans le constructeur.
Ceci est une bonne alternative à window.requestAnimationFrame
et ne présente pas les problèmes mentionnés par certains utilisateurs (il est nécessaire de le combiner avec setTimeout
ou de 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>
);
}
}
À partir de ReactDOM.render () documentation:
Si le rappel facultatif est fourni, il sera exécuté après le composant est rendu ou mis à jour.
En fait, je rencontre un problème avec un comportement similaire. Je rends un élément vidéo dans un composant avec son attribut id. Ainsi, lorsque RenderDOM.render () se termine, il charge un plug-in qui a besoin de l'id pour trouver l'espace réservé et qui échoue.
Le setTimeout avec 0ms à l'intérieur du composantDidMount () l'a corrigé :)
componentDidMount() {
if (this.props.onDidMount instanceof Function) {
setTimeout(() => {
this.props.onDidMount();
}, 0);
}
}
Un peu de mise à jour avec les classes ES6
au lieu de 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>
);
}
}
Vérifiez également les méthodes de cycle de vie des composants React: https://facebook.github.io/react/docs/react-component.html#the-component-lifecycle
Chaque composant a beaucoup de méthodes similaires à componentDidMount
par exemple.
componentWillUnmount()
- le composant est sur le point d'être supprimé du navigateur DOMJ'ai rencontré le même problème.
Dans la plupart des scénarios, l'utilisation de setTimeout(() => { }, 0)
dans componentDidMount()
de hack-ish a fonctionné.
Mais pas dans un cas particulier; et je ne voulais pas utiliser le ReachDOM findDOMNode
puisque la documentation dit:
Remarque: findDOMNode est une trappe d'échappement utilisée pour accéder au DOM sous-jacent nœud. Dans la plupart des cas, l'utilisation de cette trappe d'évacuation est déconseillée car il perce l'abstraction du composant.
(Source: https://facebook.github.io/react/docs/react-dom.html#finddomnode )
Donc, dans ce composant particulier, je devais utiliser l'événement componentDidUpdate()
. Mon code a donc ressemblé à 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'auditeur créé dans componentDidMount
:
componentWillUnmount() {
window.removeEventListener("resize", this.updateDimensions.bind(this));
}
J'ai eu une situation étrange lorsque j'ai besoin d'imprimer le composant de réaction qui reçoit une grande quantité de données et de peindre sur du canevas. J'ai essayé toutes les approches mentionnées, mais aucune d'elles n'a fonctionné de manière fiable pour moi. Avec requestAnimationFrame dans setTimeout, la toile est vide dans 20% des cas, j'ai donc procédé comme suit:
nRequest = n => range(0,n).reduce(
(acc,val) => () => requestAnimationFrame(acc), () => requestAnimationFrame(this.save)
);
Fondamentalement, j'ai créé une chaîne de requestAnimationFrame, je ne suis pas sûr que ce soit une bonne idée ou non, mais cela fonctionne dans 100% des cas pour moi jusqu'à présent (j'utilise 30 comme valeur pour n variable).
Juste pour mettre à jour un peu cette question avec les nouvelles méthodes de hook, vous pouvez simplement utiliser le 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.
[])
}
De même, si vous voulez exécuter avant le montage, utilisez le hook useLayoutEffect
:
import React, { useLayoutEffect } from 'react'
export default function App(props) {
useLayoutEffect(() => {
// your post layout code (or 'effect') here.
...
}, [])
}
Pour moi, aucune combinaison de window.requestAnimationFrame
ou setTimeout
n'a produit des résultats cohérents. Parfois cela fonctionnait, mais pas toujours - ou parfois il serait trop tard.
Je l'ai corrigé en bouclant window.requestAnimationFrame
autant de fois que nécessaire.
(Généralement 0 ou 2-3 fois)
La clé est diff > 0
: ici, nous pouvons nous assurer de la mise à jour exacte de la page.
// 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);
}
}());
}