Reagir o código "após renderização"?

367

Eu tenho um aplicativo no qual preciso definir a altura de um elemento (digamos "conteúdo do aplicativo") dinamicamente. Ele pega a altura do "cromo" do aplicativo e o subtrai e define a altura do "conteúdo do aplicativo" para caber 100% dentro dessas restrições. Isso é super simples com exibições vanilla JS, jQuery ou Backbone, mas estou lutando para descobrir qual seria o processo certo para fazer isso no React?

Abaixo está um exemplo de componente. Eu quero poder definir app-contenta altura de 100% da janela menos o tamanho de ActionBare BalanceBar, mas como sei quando tudo é renderizado e onde eu colocaria o material de cálculo nesta classe de reação?

/** @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
fonte
4
No começo, eu pensava "como o flexbox consertaria isso?" então me lembrei do recurso de coluna no Flexbox e funcionou como um encanto!
Oscar Godson

Respostas:

301

componentDidMount ()

Este método é chamado uma vez depois que seu componente é renderizado. Portanto, seu código ficaria assim.

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
fonte
214
ou componentDidUpdatese os valores puderem mudar após a primeira renderização.
Zackify 27/10/14
5
Estou tentando alterar uma propriedade css que está definida para fazer a transição, para que a animação comece após a renderização. Infelizmente, alterar o css componentDidMount()não causa uma transição.
eye_mew
8
Obrigado. O nome é tão intuitivo que me pergunto por que estava tentando nomes ridículos como "init" ou mesmo "inicializar".
Pawel
13
Mudá-lo em componentDidMount é muito rápido para o navegador. Envolva-o em um setTimeout e não dê tempo real. ou seja componentDidMount: () => { setTimeout(addClassFunction())}, ou use rAF, a resposta abaixo fornece essa resposta.
3
Isso certamente não funciona. Se você obtiver uma lista de nós e tentar iterar sobre a lista de nós, verá que o comprimento é igual a 0. Fazer setTimeout e aguardar 1 segundo funcionou para mim. Infelizmente, ele parece não ter um método que realmente espera até que o DOM seja renderizado.
NickJ
241

Uma desvantagem de usar componentDidUpdate, ou componentDidMounté que eles são realmente executados antes que os elementos dom sejam desenhados, mas depois de terem passado do React para o DOM do navegador.

Diga, por exemplo, se você precisou definir node.scrollHeight como node.scrollTop renderizado, os elementos DOM do React podem não ser suficientes. Você precisa esperar até que os elementos sejam pintados para obter sua altura.

Solução:

Use requestAnimationFramepara garantir que seu código seja executado após a pintura do seu objeto recém-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
fonte
29
window.requestAnimationFrame não foi suficiente para mim. Eu tive que cortá-lo com window.setTimeout. Argggghhhhhh !!!!!
30515 Alex
2
Ímpar. Talvez tenha mudado na versão mais recente do React, não acho que a chamada para requestAnimationFrame seja necessária. A documentação diz: "Chamado imediatamente após as atualizações do componente serem liberadas para o DOM. Este método não é chamado para a renderização inicial. Use isso como uma oportunidade para operar no DOM quando o componente tiver sido atualizado." ... estiver liberado, o nó DOM deve estar presente. - facebook.github.io/react/docs/…
Jim Soho
11
@ JimSoho, espero que você esteja certo que isso foi corrigido, mas não há realmente nada de novo nessa documentação. Isso é para casos extremos onde o dom que está sendo atualizado não é suficiente e é importante aguardar o ciclo de pintura. Tentei criar um violino com as novas versões e o velho, mas eu não poderia parecem para criar um componente bastante complexo para demonstrar o problema, mesmo indo para trás algumas versões ...
Graham P Heath
3
@neptunian Estritamente falando "" [RAF] é chamado [...] antes da próxima repintura ... "- [ developer.mozilla.org/en-US/Apps/Fundamentals/Performance/… ]. Nessa situação, o nó ainda precisa ter seu layout calculado pelo DOM (também conhecido como "refluído"). Isso usa o RAF como uma maneira de pular de antes do layout para depois do layout. A documentação do navegador de Elm é um bom lugar para mais: elmprogramming.com/virtual-dom.html#how-browsers-render-html
Graham P Heath
11
_this.getDOMNode is not a functionque diabos é esse código?
OZZIE
104

Na minha experiência, window.requestAnimationFramenão foi suficiente para garantir que o DOM foi totalmente renderizado / reflow-complete componentDidMount. Eu tenho um código em execução que acessa o DOM imediatamente após uma componentDidMountchamada e o uso exclusivo window.requestAnimationFrameresultaria na presença do elemento no DOM; no entanto, as atualizações nas dimensões do elemento ainda não são refletidas, pois ainda não ocorreu uma reflow.

A única maneira verdadeiramente confiável para isso funcionar foi agrupar meu método em a setTimeoute a window.requestAnimationFramepara garantir que a pilha de chamadas atual do React seja limpa antes de se registrar para a renderização do próximo quadro.

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

Se eu tivesse que especular por que isso está ocorrendo / necessário, eu poderia ver React em lotes de atualizações do DOM e não aplicar as alterações no DOM até que a pilha atual estivesse concluída.

Por fim, se você estiver usando medidas DOM no código que está disparando após os retornos de chamada React, provavelmente desejará usar esse método.

Elliot Chong
fonte
11
Você só precisa do setTimeout OU do requestAnimationFrame, não de ambos.
7
Normalmente, você está correto. No entanto, no contexto do método componentDidMount do React, se você anexar um requestAnimationFrame antes que a pilha seja concluída, o DOM poderá não estar totalmente atualizado. Eu tenho um código que reproduz consistentemente esse comportamento no contexto dos retornos de chamada do React. A única maneira de garantir que seu código esteja sendo executado (mais uma vez, neste caso de uso específico do React) após a atualização do DOM é deixar a pilha de chamadas limpar primeiro com um setTimeout.
Elliot Chong
6
Você notará outros comentários acima, que mencionam a necessidade da mesma solução alternativa, ou seja: stackoverflow.com/questions/26556436/react-after-render-code/… Esse é o único método 100% confiável para este caso de uso do React. Se eu tivesse que arriscar um palpite, isso pode ser devido a React atualizações em lote, que potencialmente não são aplicadas na pilha atual (portanto, adiando o requestAnimationFrame para o próximo quadro para garantir que o lote seja aplicado).
Elliot Chong
4
Eu acho que você pode precisar para retocar em seus internos JS ... altitudelabs.com/blog/what-is-the-javascript-event-loop stackoverflow.com/questions/8058612/...
Elliot Chong
2
Esperando a pilha de chamadas atual para clara não é, obviamente, uma maneira "sem sentido" para falar sobre o ciclo de eventos, mas a cada um o seu próprio eu acho ...
Elliot Chong
17

Apenas para atualizar um pouco esta questão com os novos métodos Hook, você pode simplesmente usar o 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. 
     [])
}

Além disso, se você deseja executar antes da pintura do layout, use o useLayoutEffectgancho:

import React, { useLayoutEffect } from 'react'

export default function App(props) {

     useLayoutEffect(() => {
         // your pre layout code (or 'effect') here.
         ...
     }, [])
}
P Fuster
fonte
De acordo com a documentação do React, useLayoutEffect acontece após todas as mutações do DOM reactjs.org/docs/hooks-reference.html#uselayouteffect
Philippe Hebert
É verdade, mas é executado antes que o layout tenha a chance de pintar, Updates scheduled inside useLayoutEffect will be flushed synchronously, before the browser has a chance to paint.eu vou editar.
P Fuster
Você sabe se o useEffect é executado após o reflow do navegador (e não o que o React chama de 'pintura')? É seguro solicitar o scrollHeight de um elemento com useEffect?
eMontielG
É seguro para usoEfeito
P Fuster
13

Você pode alterar o estado e, em seguida, fazer seus cálculos no retorno de chamada setState . De acordo com a documentação do React, isso é "garantido para disparar após a atualização ter sido aplicada".

Isso deve ser feito em componentDidMountou em outro local do código (como em um manipulador de eventos de redimensionamento) e não no construtor.

Essa é uma boa alternativa window.requestAnimationFramee não possui os problemas que alguns usuários mencionaram aqui (precisando combiná-lo setTimeoutou chamá-lo várias vezes). Por exemplo:

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
fonte
11
Esta é a minha resposta favorita. Código de reação idiomático limpo e bom.
phatmann
11
Esta é uma ótima resposta! Obrigado!
Bryan Jyh Herng Chong
11
Isso é lindo, vote este comentário com antecedência!
bingeScripter
11

Sinto que esta solução está suja, mas aqui vamos nós:

componentDidMount() {
    this.componentDidUpdate()
}

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

Agora vou sentar aqui e esperar pelos votos negativos.

Jaakko Karhu
fonte
5
Você deve respeitar o ciclo de vida do componente React.
Túbal Martín
2
@ TúbalMartín eu sei. Se você tem uma maneira melhor de alcançar o mesmo resultado, sinta-se à vontade para compartilhar.
Jaakko Karhu
8
Hum, um +1 figurativo para "sente-se aqui e aguarde os votos negativos". Homem corajoso. ; ^)
ruffin
14
Em vez disso, chame um método dos dois ciclos de vida, para que você não precise disparar ciclos de outros ciclos.
Tjorriemorrie
11
componentWillReceiveProps deve fazer isso #
Pablo #
8

O React possui poucos métodos de ciclo de vida que ajudam nessas situações, incluindo as listas, entre outros, getInitialState, getDefaultProps, componentWillMount, componentDidMount etc.

No seu caso e nos casos que precisam interagir com os elementos DOM, é necessário aguardar até que o dom esteja pronto; portanto, use componentDidMount como abaixo:

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

Também para obter mais informações sobre o ciclo de vida em react, você pode consultar o link abaixo: https://facebook.github.io/react/docs/state-and-lifecycle.html

getInitialState, getDefaultProps, componentWillMount, componentDidMount

Alireza
fonte
meu componente montou execuções antes que a página seja renderizada, causando um grande atraso quando uma chamada da API é carregada nos dados.
Jason G
6

Eu tive o mesmo problema.

Na maioria dos cenários usando o hack-ish setTimeout(() => { }, 0)no componentDidMount()funcionou.

Mas não em um caso especial; e eu não queria usar o, ReachDOM findDOMNodepois a documentação diz:

Nota: findDOMNode é uma hachura de escape usada para acessar o nó DOM subjacente. Na maioria dos casos, o uso dessa escotilha de escape é desencorajado porque perfura a abstração do componente.

(Fonte: findDOMNode )

Então, nesse componente em particular, eu tive que usar o componentDidUpdate()evento, então meu código acabou sendo assim:

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

E depois:

componentDidUpdate() {
    this.updateDimensions();
}

Finalmente, no meu caso, tive que remover o ouvinte criado em componentDidMount:

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

Após a renderização, você pode especificar a altura como abaixo e a altura dos componentes de reação correspondentes.

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 você pode especificar a altura de cada componente de reação usando sass. Especifique os 2 primeiros div principais do componente de reação com largura fixa e, em seguida, a altura do div principal do componente com auto. Portanto, com base no conteúdo da terceira div, a altura será atribuída.

prudhvi seeramreddi
fonte
2

Na verdade, estou tendo problemas com um comportamento semelhante; renderizo um elemento de vídeo em um Component com o atributo id, portanto, quando RenderDOM.render () termina, ele carrega um plug-in que precisa do id para encontrar o espaço reservado e não consegue encontrá-lo.

O setTimeout com 0ms dentro do componentDidMount () o corrigiu :)

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

Na documentação ReactDOM.render () :

Se o retorno de chamada opcional for fornecido, ele será executado depois que o componente for renderizado ou atualizado.

Juampy NR
fonte
9
você pode adicionar um exemplo de como usar isso? Retorno principalmente elementos do método render, não chamo render e forneço valores.
dcsan
23
Infelizmente, o retorno de chamada que você menciona só está disponível na para o nível superiorReactDOM.render , não para o de nível de componenteReactElement.render (que é o assunto aqui).
Bramus 12/07
11
O exemplo aqui seria útil
DanV
11
Cliquei no link da sua resposta e não consegui encontrar a linha que você citou, e sua resposta não inclui informações suficientes para trabalhar sem ela. Por favor, consulte stackoverflow.com/help/how-to-answer para obter conselhos sobre como escrever uma boa pergunta
Benubird 23/01/19
1

Para mim, nenhuma combinação window.requestAnimationFrameou setTimeoutproduziu resultados consistentes. Às vezes funcionava, mas nem sempre - ou às vezes seria tarde demais.

Corrigi-o repetindo window.requestAnimationFrameas vezes necessárias.
(Normalmente 0 ou 2-3 vezes)

A chave é diff > 0: aqui podemos garantir exatamente quando a página é atualizada.

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

Eu tive uma situação estranha quando preciso imprimir um componente de reação que receba grande quantidade de dados e pinte na tela. Eu tentei todas as abordagens mencionadas, nenhuma delas funcionou de maneira confiável para mim, com requestAnimationFrame dentro de setTimeout, recebo tela vazia em 20% do tempo, então fiz o seguinte:

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

Basicamente, fiz uma cadeia de requestAnimationFrame, não tenho certeza se essa é uma boa ideia ou não, mas isso funciona em 100% dos casos para mim até agora (estou usando 30 como valor para a variável n).

Anatoly Strashkevich
fonte
0

Na verdade, existe uma versão muito mais simples e mais limpa do que o uso de request animationframe ou timeouts. Estou surpreso que ninguém o tenha trazido à tona: o gerenciador de onload de vanilla-js. Se você puder, use o component mount, caso contrário, simplesmente vincule uma função no campo onload do componente jsx. Se você deseja que a função execute todas as renderizações, execute-a também antes de retornar os resultados na função de renderização. o código ficaria assim:

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

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

}

Farantir
fonte
-1

Um pouco de atualização com ES6classes em vez 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>
    );
  }
}

Além disso - verifique os métodos de ciclo de vida do componente React: O ciclo de vida do componente

Cada componente tem muitos métodos semelhantes aos do componentDidMounteg.

  • componentWillUnmount() - o componente está prestes a ser removido do DOM do navegador
pie6k
fonte
11
Sem desrespeito, mas como isso responde à pergunta? Mostrar uma atualização no ES6 não está realmente relacionado à pergunta / não muda nada. Todas as respostas mais antigas já falam sobre como o componentDidMount não funciona por si só.
Dave4jr