¿Reacciona el código "después de renderizar"?

367

Tengo una aplicación donde necesito establecer la altura de un elemento (digamos "contenido de la aplicación") dinámicamente. Toma la altura del "cromo" de la aplicación y la resta y luego establece la altura del "contenido de la aplicación" para que se ajuste al 100% dentro de esas restricciones. Esto es súper simple con las vistas vanilla JS, jQuery o Backbone, pero estoy luchando por descubrir cuál sería el proceso correcto para hacer esto en React.

A continuación se muestra un componente de ejemplo. Quiero poder configurar app-contentla altura para que sea el 100% de la ventana menos el tamaño de ActionBary BalanceBar, pero ¿cómo sé cuándo se representa todo y dónde colocaría el material de cálculo en esta clase de reacción?

/** @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
fuente
44
Al principio pensé "¿cómo solucionaría esto Flexbox?" ¡Entonces recordé la función de columna en Flexbox y funcionó de maravilla!
Oscar Godson

Respuestas:

301

componentDidMount ()

Este método se llama una vez después de que se representa su componente. Entonces su código se vería así.

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
fuente
214
o componentDidUpdatesi los valores pueden cambiar después del primer render.
zackify
55
Estoy tratando de cambiar una propiedad css que está configurada para transición, para que la animación comience después de la representación. Desafortunadamente, cambiar el CSS en componentDidMount()no causa una transición.
eye_mew
8
Gracias. El nombre es tan intuitivo que me pregunto por qué estaba probando nombres ridículos como "init" o incluso "initialize".
Pawel
13
Cambiarlo en componentDidMount es demasiado rápido para el navegador. Envuélvelo en un setTimeout y no le des tiempo real. es decir componentDidMount: () => { setTimeout(addClassFunction())}, o use rAF, la respuesta a continuación proporciona esta respuesta.
3
Esto ciertamente no funciona. Si obtiene una lista de nodos y luego intenta iterar sobre la lista de nodos, encontrará que la longitud es igual a 0. Hacer setTimeout y esperar 1 segundo funcionó para mí. Desafortunadamente, no parece que reaccionar tiene un método que realmente espera hasta que se procesa el DOM.
NickJ
241

Un inconveniente del uso componentDidUpdate, o componentDidMountes que en realidad se ejecutan antes de que se dibujen los elementos dom, pero después de que se hayan pasado de React al DOM del navegador.

Digamos, por ejemplo, si necesita establecer node.scrollHeight en el nodo renderizado.scrollTop, entonces los elementos DOM de React pueden no ser suficientes. Debe esperar hasta que los elementos estén pintados para obtener su altura.

Solución:

Utilícelo requestAnimationFramepara asegurarse de que su código se ejecute después de pintar su objeto recién renderizado

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
fuente
29
window.requestAnimationFrame no fue suficiente para mí. Tuve que hackearlo con window.setTimeout. Argggghhhhhh !!!!!
Alex
2
Impar. Tal vez ha cambiado en la versión más reciente de React, no creo que sea necesario llamar a requestAnimationFrame. La documentación dice: "Se invoca inmediatamente después de que las actualizaciones del componente se vacíen al DOM. Este método no se requiere para el procesamiento inicial. Use esto como una oportunidad para operar en el DOM cuando el componente se haya actualizado". está enjuagado, el nodo DOM debe estar presente. - facebook.github.io/react/docs/…
Jim Soho
1
@ JimSoho, espero que tengas razón en que esto se solucionó, pero en realidad no hay nada nuevo en esa documentación. Esto es para casos extremos donde la actualización del dom no es suficiente, y es importante que esperemos el ciclo de pintura. Traté de crear un violín con las nuevas versiones y las antiguas, pero no pude crear un componente lo suficientemente complejo como para demostrar el problema, incluso volviendo a algunas versiones ...
Graham P Heath
3
@neptunian Hablando estrictamente "[RAF] se llama [...] antes del próximo repintado ..." - [ developer.mozilla.org/en-US/Apps/Fundamentals/Performance/… ]. En esta situación, el nodo aún necesita que el DOM calcule su diseño (también conocido como "refluido"). Esto usa RAF como una forma de saltar desde antes del diseño hasta después del diseño. La documentación del navegador de Elm es un buen lugar para obtener más información: elmprogramming.com/virtual-dom.html#how-browsers-render-html
Graham P Heath
1
_this.getDOMNode is not a function¿Qué diablos es este código?
OZZIE
104

En mi experiencia window.requestAnimationFrame, no fue suficiente para garantizar que el DOM se haya procesado / reflujo completo desde componentDidMount. Tengo un código en ejecución que accede al DOM inmediatamente después de una componentDidMountllamada y el uso exclusivo window.requestAnimationFramedaría como resultado que el elemento esté presente en el DOM; sin embargo, las actualizaciones de las dimensiones del elemento aún no se reflejan ya que aún no se ha producido un reflujo.

La única forma verdaderamente fiable para que esto funcione era de envolver mi método en una setTimeouty una window.requestAnimationFramepara asegurar Reaccionar de pila de llamadas actual se limpia antes de registrarse para el siguiente fotograma de render.

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

Si tuviera que especular sobre por qué esto está ocurriendo / es necesario, podría ver Reaccionar actualizaciones de DOM por lotes y no aplicar los cambios al DOM hasta que se complete la pila actual.

En última instancia, si está utilizando mediciones DOM en el código que está disparando después de las devoluciones de llamada React, es probable que desee utilizar este método.

Elliot Chong
fuente
1
Solo necesita setTimeout O requestAnimationFrame, no ambos.
77
Por lo general, tienes razón. Sin embargo, en el contexto del método componentDidMount de React, si adjunta un requestAnimationFrame antes de que finalice la pila, el DOM puede no actualizarse completamente. Tengo un código que reproduce constantemente este comportamiento dentro del contexto de las devoluciones de llamada de React. La única forma de asegurarse de que su código se esté ejecutando (una vez más, en este caso de uso específico de React) después de que el DOM se haya actualizado, dejando que la pila de llamadas se elimine primero con un setTimeout.
Elliot Chong
66
Notará otros comentarios anteriores que mencionan la necesidad de la misma solución alternativa, es decir: stackoverflow.com/questions/26556436/react-after-render-code/... Este es el único método 100% confiable para este caso de uso de React. Si tuviera que aventurar una suposición, podría deberse a las actualizaciones de React batching que posiblemente no se apliquen dentro de la pila actual (por lo tanto, diferir el requestAnimationFrame al siguiente marco para garantizar que se aplique el lote).
Elliot Chong
44
Creo que puede que tenga que poner al día sus partes internas JS ... altitudelabs.com/blog/what-is-the-javascript-event-loop stackoverflow.com/questions/8058612/...
Elliot Chong
2
Esperar a que se elimine la pila de llamadas actual obviamente no es una forma "sin sentido" de hablar sobre el bucle de eventos, pero creo que a cada uno lo suyo ...
Elliot Chong
17

Solo para actualizar un poco esta pregunta con los nuevos métodos de gancho, simplemente puede usar el useEffectgancho:

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

Además, si desea ejecutar antes de la pintura de diseño, use el useLayoutEffectgancho:

import React, { useLayoutEffect } from 'react'

export default function App(props) {

     useLayoutEffect(() => {
         // your pre layout code (or 'effect') here.
         ...
     }, [])
}
P Fuster
fuente
Según la documentación de React, useLayoutEffect ocurre después de todas las mutaciones DOM reactjs.org/docs/hooks-reference.html#uselayouteffect
Philippe Hebert
Es cierto, pero se ejecuta antes de que el diseño tenga la oportunidad de pintar Updates scheduled inside useLayoutEffect will be flushed synchronously, before the browser has a chance to paint.que editaré.
P Fuster
¿Sabes si useEffect se ejecuta después del reflujo del navegador (no lo que React llama 'paint')? ¿Es seguro solicitar scrollHeight de un elemento con useEffect?
eMontielG
Es seguro para su uso Efecto
P Fuster
13

Puede cambiar el estado y luego hacer sus cálculos en la devolución de llamada setState . De acuerdo con la documentación de React, "se garantiza que se activará después de que se haya aplicado la actualización".

Esto debe hacerse en componentDidMounto en algún otro lugar del código (como en un controlador de eventos de cambio de tamaño) en lugar de hacerlo en el constructor.

Esta es una buena alternativa window.requestAnimationFramey no tiene los problemas que algunos usuarios han mencionado aquí (es necesario combinarlo setTimeouto llamarlo varias veces). Por ejemplo:

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
fuente
1
Esta es mi respuesta favorita. Limpio y buen código de reacción idiomático.
phatmann
1
¡Esta es una respuesta genial! ¡Gracias!
Bryan Jyh Herng Chong
1
Esto es hermoso, ¡vota este comentario!
bingeScripter
11

Siento que esta solución está sucia, pero aquí vamos:

componentDidMount() {
    this.componentDidUpdate()
}

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

Ahora me voy a sentar aquí y esperar los votos negativos.

Jaakko Karhu
fuente
55
Debe respetar un ciclo de vida del componente React.
Túbal Martín
2
@ TúbalMartín lo sé. Si tiene una mejor manera de alcanzar el mismo resultado, no dude en compartir.
Jaakko Karhu
8
Um, un +1 figurativo para "siéntate aquí y espera los votos negativos". Hombre valiente. ; ^)
ruffin
14
En su lugar, llame a un método de ambos ciclos de vida, entonces no tiene que activar ciclos de otros ciclos.
Tjorriemorrie
1
componentWillReceiveProps debería hacer esto
Pablo
8

React tiene pocos métodos de ciclo de vida que ayudan en estas situaciones, las listas incluyen, entre otras, getInitialState, getDefaultProps, componentWillMount, componentDidMount, etc.

En su caso y los casos que necesitan interactuar con los elementos DOM, debe esperar hasta que el dom esté listo, por lo tanto, use componentDidMount como se muestra a continuación:

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

También para obtener más información sobre el ciclo de vida en reaccionar, puede consultar el siguiente enlace: https://facebook.github.io/react/docs/state-and-lifecycle.html

getInitialState, getDefaultProps, componentWillMount, componentDidMount

Alireza
fuente
mi componente montó ejecuciones antes de que la página se procese, lo que causó un gran retraso cuando se carga una llamada de API en los datos.
Jason G
6

Tuve el mismo problema.

En la mayoría de los escenarios usando el hack-ish setTimeout(() => { }, 0)en componentDidMount()trabajado.

Pero no en un caso especial; y no quería usar el ReachDOM findDOMNodepuesto que la documentación dice:

Nota: findDOMNode es una trampilla de escape utilizada para acceder al nodo DOM subyacente. En la mayoría de los casos, se desaconseja el uso de esta trampilla de escape porque perfora la abstracción del componente.

(Fuente: findDOMNode )

Entonces, en ese componente en particular, tuve que usar el componentDidUpdate()evento, por lo que mi código terminó siendo así:

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

Y entonces:

componentDidUpdate() {
    this.updateDimensions();
}

Finalmente, en mi caso, tuve que eliminar el oyente creado en componentDidMount:

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

Después de renderizar, puede especificar la altura como se muestra a continuación y puede especificar la altura a los componentes de reacción correspondientes.

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

o puede especificar la altura de cada componente de reacción usando sass. Especifique los primeros 2 divisores principales del componente de reacción con ancho fijo y luego la altura del tercer componente principal con auto. Entonces, según el contenido del tercer div, se asignará la altura.

Prudhvi seeramreddi
fuente
2

En realidad, estoy teniendo problemas con un comportamiento similar, renderizo un elemento de video en un Componente con su atributo de identificación, por lo que cuando RenderDOM.render () finaliza, carga un complemento que necesita la identificación para encontrar el marcador de posición y no lo encuentra.

El setTimeout con 0ms dentro del componentDidMount () lo arregló :)

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

De la documentación de ReactDOM.render () :

Si se proporciona la devolución de llamada opcional, se ejecutará después de que el componente se procese o actualice.

Juampy NR
fuente
99
¿Puedes agregar un ejemplo de cómo usar esto? Principalmente devuelvo elementos del método de renderizado, no llamo render y proporciono valores.
dcsan
23
Por desgracia, la devolución de llamada que mencionas sólo está disponible en el para el nivel superiorReactDOM.render , no para el de nivel de componenteReactElement.render (que es el tema aquí).
Bramus
1
Ejemplo aquí sería útil
DanV
1
Hice clic en el enlace en su respuesta, y no pude encontrar la línea que citó, y su respuesta no incluye suficiente información para trabajar sin ella. Consulte stackoverflow.com/help/how-to-answer para obtener consejos sobre cómo escribir una buena pregunta
Benubird el
1

Para mí, ninguna combinación de window.requestAnimationFrameo setTimeoutproduce resultados consistentes. A veces funcionó, pero no siempre, o a veces sería demasiado tarde.

Lo arreglé haciendo un bucle window.requestAnimationFrametantas veces como sea necesario.
(Típicamente 0 o 2-3 veces)

La clave es diff > 0: aquí podemos asegurarnos exactamente cuando la página se actualice.

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

Tuve una situación extraña cuando necesito imprimir un componente de reacción que recibe una gran cantidad de datos y pintar en el lienzo. He probado todos los enfoques mencionados, ninguno de ellos funcionó de manera confiable para mí, con requestAnimationFrame dentro de setTimeout obtengo un lienzo vacío en el 20% del tiempo, así que hice lo siguiente:

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

Básicamente hice una cadena de requestAnimationFrame's, no estoy seguro de si es una buena idea o no, pero hasta ahora funciona en el 100% de los casos (estoy usando 30 como valor para la variable n).

Anatoly Strashkevich
fuente
0

En realidad, hay una versión mucho más simple y limpia que usar el marco de animación de solicitud o los tiempos de espera. Iam se sorprendió de que nadie lo mencionara: el controlador de carga vanilla-js. Si puede, use el componente sí se montó, si no, simplemente vincule una función en el controlador de carga del componente jsx. Si desea que la función ejecute cada render, también ejecútela antes de devolverle los resultados en la función de render. el código se vería así:

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

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

}

Farantir
fuente
-1

Un poco de actualización con ES6clases en lugar 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>
    );
  }
}

Además, verifique los métodos del ciclo de vida del componente React: el ciclo de vida del componente

Cada componente tiene muchos métodos similares a, componentDidMountpor ejemplo.

  • componentWillUnmount() - el componente está a punto de eliminarse del navegador DOM
pie6k
fuente
1
No hay falta de respeto, pero ¿cómo responde esto a la pregunta? Mostrar una actualización en ES6 no está realmente relacionado con la pregunta / no cambia nada. Todas las respuestas mucho más antiguas ya hablan sobre cómo componentDidMount no funciona por sí solo.
dave4jr