Bereaksi kode "setelah render"?

367

Saya memiliki aplikasi tempat saya perlu mengatur ketinggian elemen (katakanlah "konten-aplikasi") secara dinamis. Dibutuhkan ketinggian "chrome" dari aplikasi dan kurangi itu dan kemudian atur ketinggian "app-content" agar sesuai dengan 100% dalam batasan itu. Ini super sederhana dengan tampilan vanilla JS, jQuery, atau Backbone, tapi saya berjuang untuk mencari tahu proses apa yang tepat untuk melakukan ini di React?

Di bawah ini adalah contoh komponen. Saya ingin dapat mengatur app-contenttinggi menjadi 100% dari jendela dikurangi ukuran ActionBardan BalanceBar, tetapi bagaimana saya tahu kapan semuanya dirender dan di mana saya akan meletakkan barang perhitungan di Kelas Bereaksi ini?

/** @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
sumber
4
Awalnya saya seperti "bagaimana flexbox memperbaiki ini?" kemudian saya ingat fitur kolom di Flexbox dan itu berfungsi seperti pesona!
Oscar Godson

Jawaban:

301

componentDidMount ()

Metode ini dipanggil sekali setelah komponen Anda dirender. Jadi kode Anda akan terlihat seperti itu.

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
sumber
214
atau componentDidUpdatejika nilai dapat berubah setelah render pertama.
zackify
5
Saya mencoba mengubah properti css yang diatur ke transisi, sehingga animasi dimulai setelah rendering. Sayangnya, mengubah css componentDidMount()tidak menyebabkan transisi.
eye_mew
8
Terima kasih. Namanya sangat intuitif sehingga saya bertanya-tanya mengapa saya mencoba nama-nama konyol seperti "init" atau bahkan "inisialisasi".
Pawel
13
Mengubahnya di componentDidMount terlalu cepat untuk browser. Bungkus dalam setTimeout dan jangan berikan waktu aktual. yaitu componentDidMount: () => { setTimeout(addClassFunction())}, atau menggunakan rAF, jawaban di bawah ini memberikan jawaban ini.
3
Ini pasti TIDAK bekerja. Jika Anda mendapatkan daftar simpul dan kemudian mencoba untuk mengulangi daftar simpul Anda akan menemukan panjangnya sama dengan 0. Melakukan setTimeout dan menunggu 1 detik bekerja untuk saya. Sayangnya itu tidak muncul bereaksi memiliki metode yang benar-benar menunggu sampai setelah DOM diberikan.
NickJ
241

Salah satu kelemahan menggunakan componentDidUpdate, atau componentDidMountmereka benar-benar dieksekusi sebelum elemen dom selesai dibuat, tetapi setelah mereka telah diteruskan dari Bereaksi ke DOM browser.

Katakan misalnya jika Anda perlu mengatur node.scrollHeight ke node.scrollTop yang diberikan, maka elemen DOM Bereaksi mungkin tidak cukup. Anda harus menunggu sampai elemen selesai dicat untuk mendapatkan ketinggiannya.

Larutan:

Gunakan requestAnimationFrameuntuk memastikan bahwa kode Anda dijalankan setelah lukisan objek yang baru Anda render

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
sumber
29
window.requestAnimationFrame tidak cukup bagi saya. Saya harus meretasnya dengan window.setTimeout. Argggghhhhhh !!!!!
Alex
2
Aneh. Mungkin itu telah berubah di React versi terbaru, saya tidak berpikir panggilan untuk memintaAnimationFrame diperlukan. Dokumentasi mengatakan: "Diminta segera setelah pembaruan komponen disiram ke DOM. Metode ini tidak dipanggil untuk render awal. Gunakan ini sebagai kesempatan untuk beroperasi pada DOM ketika komponen telah diperbarui." ... yaitu, itu memerah, simpul DOM harus ada. - facebook.github.io/react/docs/…
Jim Soho
1
@ JimSoho, saya harap Anda benar bahwa ini diperbaiki, tetapi sebenarnya tidak ada yang baru dalam dokumentasi itu. Ini untuk kasus tepi di mana dom sedang diperbarui tidak cukup, dan penting bahwa kita menunggu siklus cat. Saya mencoba membuat biola dengan versi yang baru dan yang lama, tetapi sepertinya saya tidak bisa membuat komponen yang cukup kompleks untuk menunjukkan masalah, bahkan kembali beberapa versi ...
Graham P Heath
3
@neptunian Sebenarnya, "[RAF] dipanggil [...] sebelum dicat ulang ..." - [ developer.mozilla.org/en-US/Apps/Fundamentals/Performance/… ]. Dalam situasi ini node masih perlu tata letaknya dihitung oleh DOM (alias "reflowed"). Ini menggunakan RAF sebagai cara melompat dari sebelum tata letak ke setelah tata letak. Dokumentasi peramban dari Elm adalah tempat yang bagus untuk lebih banyak: elmprogramming.com/virtual-dom.html#how-browsers-render-html
Graham P Heath
1
_this.getDOMNode is not a functionapa kode ini?
OZZIE
104

Dalam pengalaman saya window.requestAnimationFrametidak cukup untuk memastikan bahwa DOM telah sepenuhnya diberikan / reflow-complete from componentDidMount. Saya memiliki kode yang menjalankan yang mengakses DOM segera setelah componentDidMountpanggilan dan menggunakan hanya window.requestAnimationFrameakan menghasilkan elemen yang ada di DOM; namun, pembaruan dimensi elemen belum tercermin karena belum ada reflow.

Satu-satunya cara yang benar-benar dapat diandalkan untuk bekerja adalah membungkus metode saya dalam setTimeoutdan window.requestAnimationFrameuntuk memastikan tumpukan panggilan React saat ini akan dihapus sebelum mendaftar untuk render frame berikutnya.

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

Jika saya harus berspekulasi tentang mengapa ini terjadi / perlu saya bisa melihat Bereaksi DOM pembaruan batching dan tidak benar-benar menerapkan perubahan ke DOM sampai setelah tumpukan saat ini selesai.

Pada akhirnya, jika Anda menggunakan pengukuran DOM dalam kode yang Anda jalankan setelah panggilan balik Bereaksi Anda mungkin ingin menggunakan metode ini.

Elliot Chong
sumber
1
Anda hanya perlu setTimeout ATAU requestAnimationFrame, bukan keduanya.
7
Biasanya - Anda benar. Namun, dalam konteks metode componentDidMount React jika Anda melampirkan requestAnimationFrame sebelum tumpukan selesai, DOM mungkin sebenarnya tidak sepenuhnya diperbarui. Saya memiliki kode yang secara konsisten mereproduksi perilaku ini dalam konteks panggilan balik React. Satu-satunya cara untuk memastikan kode Anda dieksekusi (sekali lagi, dalam kasus penggunaan khusus Bereaksi ini) setelah DOM diperbarui adalah membiarkan tumpukan panggilan dihapus terlebih dahulu dengan setTimeout.
Elliot Chong
6
Anda akan melihat komentar lain di atas yang menyebutkan membutuhkan solusi yang sama, yaitu: stackoverflow.com/questions/26556436/react-after-render-code/… Ini adalah satu-satunya metode yang dapat diandalkan 100% untuk kasus penggunaan yang Bereaksi ini. Jika saya harus berani menebak itu mungkin karena Bereaksi pembaruan batching sendiri yang berpotensi tidak diterapkan dalam tumpukan saat ini (maka tunda permintaanAnimationFrame ke frame berikutnya untuk memastikan batch diterapkan).
Elliot Chong
4
Saya pikir Anda mungkin perlu memoles internal JS Anda ... altitudelabs.com/blog/what-is-the-javascript-event-loop stackoverflow.com/questions/8058612/…
Elliot Chong
2
Menunggu tumpukan panggilan saat ini jelas bukan cara yang "tidak masuk akal" untuk membicarakan perulangan acara, tetapi untuk masing-masing acara, saya kira ...
Elliot Chong
17

Hanya untuk memperbarui sedikit pertanyaan ini dengan metode Hook baru, Anda cukup menggunakan 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. 
     [])
}

Juga jika Anda ingin menjalankan sebelum cat tata letak menggunakan useLayoutEffectkait:

import React, { useLayoutEffect } from 'react'

export default function App(props) {

     useLayoutEffect(() => {
         // your pre layout code (or 'effect') here.
         ...
     }, [])
}
P Fuster
sumber
Sesuai dokumentasi React, useLayoutEffect terjadi setelah semua mutasi DOM reactjs.org/docs/hooks-reference.html#uselayouteffect
Philippe Hebert
Benar, tetapi itu berjalan sebelum tata letak memiliki kesempatan untuk mengecat Updates scheduled inside useLayoutEffect will be flushed synchronously, before the browser has a chance to paint.saya akan mengedit.
P Fuster
Apakah Anda tahu jika useEffect berjalan setelah reflow browser (bukan apa yang React sebut 'cat')? Apakah aman untuk meminta scroll elemenHeight dengan useEffect?
eMontielG
Aman untuk digunakanEffect
P Fuster
13

Anda dapat mengubah status dan kemudian melakukan perhitungan di callback setState . Menurut dokumentasi Bereaksi, ini "dijamin akan diaktifkan setelah pembaruan diterapkan".

Ini harus dilakukan di componentDidMountatau di tempat lain dalam kode (seperti pada event handler ukuran) daripada di konstruktor.

Ini adalah alternatif yang baik window.requestAnimationFramedan tidak memiliki masalah yang disebutkan beberapa pengguna di sini (perlu untuk menggabungkannya setTimeoutatau menyebutnya beberapa kali). Sebagai contoh:

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
sumber
1
Ini jawaban favorit saya. Kode Bereaksi idiomatis yang bersih dan baik.
phatmann
1
Ini jawaban yang bagus! Terima kasih!
Bryan Jyh Herng Chong
1
Ini indah, tepuk tangan komentar ini!
bingeScripter
11

Saya merasa bahwa solusi ini kotor, tetapi kita mulai:

componentDidMount() {
    this.componentDidUpdate()
}

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

Sekarang saya hanya akan duduk di sini dan menunggu suara turun.

Jaakko Karhu
sumber
5
Anda harus menghormati siklus hidup komponen React.
Túbal Martín
2
@ TúbalMartín saya tahu. Jika Anda memiliki cara yang lebih baik untuk mencapai hasil yang sama, silakan berbagi.
Jaakko Karhu
8
Um, +1 figuratif untuk "duduk di sini dan tunggu suara turun". Lelaki pemberani. ; ^)
ruffin
14
Alih-alih memanggil metode dari kedua siklus, maka Anda tidak harus memicu siklus dari siklus lain.
Tjorriemorrie
1
componentWillReceiveProps harus melakukan ini
Pablo
8

Bereaksi memiliki beberapa metode siklus hidup yang membantu dalam situasi ini, daftar termasuk tetapi tidak terbatas pada getInitialState, getDefaultProps, componentWillMount, componentDidMount dll.

Dalam kasus Anda dan kasus-kasus yang perlu berinteraksi dengan elemen DOM, Anda harus menunggu sampai dom siap, jadi gunakan componentDidMount seperti di bawah ini:

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

Juga untuk informasi lebih lanjut tentang siklus hidup dalam reaksi Anda dapat melihat tautan di bawah ini: https://facebook.github.io/react/docs/state-and-lifecycle.html

getInitialState, getDefaultProps, componentWillMount, componentDidMount

Alireza
sumber
komponen saya memang me-mount berjalan sebelum membuat halaman menyebabkan penundaan besar sebagai panggilan api memuat data.
Jason G
6

Saya mengalami masalah yang sama.

Dalam kebanyakan skenario menggunakan hack-ish setTimeout(() => { }, 0)di componentDidMount()bekerja.

Tetapi tidak dalam kasus khusus; dan saya tidak ingin menggunakan ReachDOM findDOMNodekarena dokumentasi mengatakan:

Catatan: findDOMNode adalah jalan keluar yang digunakan untuk mengakses simpul DOM yang mendasarinya. Dalam kebanyakan kasus, penggunaan pintu darurat ini tidak disarankan karena menembus abstraksi komponen.

(Sumber: findDOMNode )

Jadi dalam komponen tertentu saya harus menggunakan componentDidUpdate()acara tersebut, jadi kode saya akhirnya menjadi seperti ini:

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

Lalu:

componentDidUpdate() {
    this.updateDimensions();
}

Akhirnya, dalam kasus saya, saya harus menghapus pendengar yang dibuat di componentDidMount:

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

Setelah render, Anda dapat menentukan ketinggian seperti di bawah ini dan dapat menentukan ketinggian untuk komponen reaksi yang sesuai.

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

atau Anda dapat menentukan ketinggian masing-masing komponen reaksi menggunakan sass. Tentukan div 2 utama komponen reaksi pertama dengan lebar tetap dan kemudian tinggi komponen utama 3 reaksi dengan otomatis. Jadi berdasarkan konten div ketiga ketinggian akan ditetapkan.

prudhvi seeramreddi
sumber
2

Saya sebenarnya mengalami masalah dengan perilaku yang serupa, saya membuat elemen video dalam Komponen dengan atribut id itu sehingga ketika RenderDOM.render () mengakhirinya memuat plugin yang memerlukan id untuk menemukan placeholder dan gagal menemukannya.

SetTimeout dengan 0ms di dalam componentDidMount () memperbaikinya :)

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

Dari dokumentasi ReactDOM.render () :

Jika callback opsional diberikan, itu akan dieksekusi setelah komponen diberikan atau diperbarui.

Juampy NR
sumber
9
dapatkah Anda menambahkan contoh cara menggunakannya? Saya kebanyakan mengembalikan elemen dari metode render, saya tidak memanggil render dan memberikan nilai.
dcsan
23
Sayangnya callback yang Anda sebutkan hanya tersedia di untuk tingkat atasReactDOM.render , bukan untuk tingkat komponenReactElement.render (yang menjadi subjek di sini).
Bramus
1
Contoh di sini akan membantu
DanV
1
Saya mengklik tautan dalam jawaban Anda, dan saya tidak dapat menemukan baris yang Anda kutip, dan jawaban Anda tidak menyertakan informasi yang cukup untuk bekerja tanpanya. Silakan lihat stackoverflow.com/help/how-to-answer untuk nasihat tentang cara menulis pertanyaan yang bagus
Benubird
1

Bagi saya, tidak ada kombinasi window.requestAnimationFrameatausetTimeout hasil yang konsisten. Terkadang itu berhasil, tetapi tidak selalu — atau kadang-kadang akan terlambat.

Saya memperbaikinya dengan mengulang window.requestAnimationFramesebanyak yang diperlukan.
(Biasanya 0 atau 2-3 kali)

Kuncinya adalah diff > 0: di sini kita dapat memastikan kapan halaman diperbarui.

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

Saya punya situasi aneh ketika saya perlu mencetak komponen reaksi yang menerima sejumlah besar data dan melukis di atas kanvas. Saya sudah mencoba semua pendekatan yang disebutkan, tidak ada yang bekerja dengan baik untuk saya, dengan requestAnimationFrame di dalam setTimeout saya mendapatkan kanvas kosong dalam 20% dari waktu, jadi saya melakukan yang berikut:

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

Pada dasarnya saya membuat rantai requestAnimationFrame's, tidak yakin apakah ini ide yang bagus atau tidak, tetapi ini berfungsi dalam 100% kasus untuk saya sejauh ini (saya menggunakan 30 sebagai nilai untuk variabel n).

Anatoly Strashkevich
sumber
0

Sebenarnya ada versi yang jauh lebih sederhana dan lebih bersih daripada menggunakan kerangka animasi permintaan atau batas waktu. Iam mengejutkan tidak ada yang membawanya: pawang vanilla-js onload. Jika Anda bisa, gunakan komponen yang melakukan mount, jika tidak, cukup ikat fungsi pada onload hanlder dari komponen jsx. Jika Anda ingin fungsi menjalankan setiap render, jalankan juga sebelum mengembalikan Anda menghasilkan fungsi render. kode akan terlihat seperti ini:

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

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

}

Farantir
sumber
-1

Sedikit pembaruan dengan ES6kelas, bukanReact.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>
    );
  }
}

Juga - periksa Bereaksi metode siklus hidup komponen: Siklus Hidup Komponen

Setiap komponen memiliki banyak metode yang mirip dengan componentDidMountmis.

  • componentWillUnmount() - komponen akan dihapus dari browser DOM
pie6k
sumber
1
Tidak ada rasa tidak hormat, tetapi bagaimana ini menjawab pertanyaan? Menampilkan pembaruan pada ES6 tidak benar-benar terkait dengan pertanyaan / tidak mengubah apa pun. Semua jawaban yang jauh lebih tua sudah membicarakan tentang bagaimana componentDidMount tidak bekerja sendiri.
dave4jr