React - Bagaimana cara mendeteksi ketika semua sub-komponen komponen induk terlihat oleh pengguna?

11

TL; DR

Bagaimana komponen induk dapat mengetahui kapan rendering setiap komponen anak di bawahnya telah selesai dan DOM terlihat oleh pengguna dengan versi terbarunya?

Katakanlah saya memiliki komponen component Aanak yang Gridterdiri dari 3x3komponen cucu. Masing-masing komponen cucu itu mengambil data dari titik akhir API yang tenang dan membuatnya sendiri ketika data tersedia.

Saya ingin mencakup seluruh area Component Adengan loaderplaceholder, untuk diungkapkan hanya ketika komponen terakhir dalam kisi telah mengambil data dengan sukses, dan mengolahnya, sehingga sudah ada di DOM dan dapat dilihat.

Pengalaman pengguna harus menjadi transisi yang sangat mulus dari "loader" ke grid yang terisi penuh tanpa berkedip.

Masalah saya adalah mengetahui kapan tepatnya mengungkap komponen di bawah loader.

Apakah ada mekanisme yang bisa saya andalkan untuk melakukan ini dengan akurasi absolut? Saya tidak membuat kode batas waktu untuk loader. Seperti yang saya pahami bergantung ComponentDidMountuntuk setiap anak juga tidak dapat diandalkan karena sebenarnya tidak menjamin komponen sepenuhnya terlihat oleh pengguna pada saat panggilan.

Untuk menyaring pertanyaan lebih jauh:

Saya memiliki komponen yang membuat beberapa jenis data. Setelah diinisialisasi itu tidak memilikinya, jadi di componentDidMountdalamnya hits titik akhir API untuk itu. Setelah menerima data, itu mengubah statusnya untuk mencerminkannya. Hal ini dapat dimengerti menyebabkan re-render dari keadaan akhir komponen itu. Pertanyaan saya adalah ini: Bagaimana saya tahu kapan re-render telah terjadi dan tercermin dalam Pengguna yang menghadapi DOM. Titik waktu itu! = Titik waktu ketika keadaan komponen telah berubah menjadi berisi data.

JasonGenX
sumber
Bagaimana mengelola negara? Suka menggunakan Redux? atau itu murni dalam komponen?
TechTurtle
Itu ada di dalam komponen, tetapi bisa dieksternalisasi. Saya menggunakan Redux untuk hal-hal lain.
JasonGenX
dan bagaimana Anda tahu bahwa komponen terakhir dalam kisi telah selesai mengambil data?
TechTurtle
Bukan saya. Komponen grid tidak saling menyadari. Di setiap sub-komponen yang ComponentDidMountsaya gunakan axiosuntuk mengambil data. ketika data datang, saya mengubah status komponen yang menyebabkan render data. Secara teori, 8 komponen anak dapat diambil dalam 3 detik, dan yang terakhir akan memakan waktu 15 detik ...
JasonGenX
1
Saya bisa melakukan itu, tetapi bagaimana saya tahu kapan harus mengungkap overload loader sehingga pengguna tidak akan pernah melihat bagaimana komponen berubah dari "kosong" menjadi "penuh"? Ini bukan pertanyaan tentang pengambilan data. Ini tentang render ... bahkan jika saya hanya memiliki satu komponen anak. ComponentDidMount tidak cukup. Saya perlu tahu kapan post-data-fetching-render telah selesai dan dan DOM telah diperbarui sepenuhnya sehingga saya dapat mengungkap overlay loader.
JasonGenX

Jawaban:

5

Ada dua kait siklus hidup di Bereaksi yang dipanggil setelah DOM komponen diberikan:

Untuk kasus penggunaan Anda, komponen induk Anda P tertarik ketika N komponen anak masing-masing puas beberapa kondisi X . X dapat didefinisikan sebagai urutan:

  • operasi async selesai
  • komponen telah ditampilkan

Dengan menggabungkan keadaan komponen dan menggunakan componentDidUpdate pengait, Anda dapat mengetahui kapan urutannya selesai dan komponen Anda memenuhi kondisi X.

Anda dapat melacak kapan operasi async Anda selesai dengan menetapkan variabel status. Sebagai contoh:

this.setState({isFetched: true})

Setelah mengatur keadaan, Bereaksi akan memanggil componentDidUpdatefungsi komponen Anda . Dengan membandingkan objek keadaan saat ini dan sebelumnya dalam fungsi ini, Anda dapat memberi sinyal ke komponen induk bahwa operasi async Anda telah selesai dan status komponen baru Anda telah dirender:

componentDidUpdate(_prevProps, prevState) {
  if (this.state.isFetched === true && this.state.isFetched !== prevState.isFetched) {
    this.props.componentHasMeaningfullyUpdated()
  }
}

Dalam komponen P Anda, Anda dapat menggunakan penghitung untuk melacak berapa banyak anak yang diperbarui secara bermakna:

function onComponentHasMeaningfullyUpdated() {
  this.setState({counter: this.state.counter + 1})
}

Akhirnya, dengan mengetahui panjang N Anda dapat mengetahui kapan semua pembaruan yang berarti telah terjadi dan bertindak sesuai dalam metode render P Anda :

const childRenderingFinished = this.state.counter >= N
Andrew Sinner
sumber
1

Saya akan mengaturnya sehingga Anda mengandalkan variabel kondisi global untuk memberi tahu komponen Anda kapan harus merender. Redux lebih baik untuk skenario ini di mana banyak komponen berbicara satu sama lain, dan Anda menyebutkan dalam komentar bahwa Anda kadang-kadang menggunakannya. Jadi saya akan membuat sketsa jawaban menggunakan Redux.

Anda harus memindahkan panggilan API ke wadah induk, Component A ,. Jika Anda ingin agar cucu-cucu Anda render hanya setelah panggilan API selesai, Anda tidak dapat menyimpan panggilan API itu sendiri di cucu. Bagaimana panggilan API dapat dibuat dari komponen yang belum ada?

Setelah semua panggilan API dilakukan, Anda dapat menggunakan tindakan untuk memperbarui variabel status global yang berisi banyak objek data. Setiap kali data diterima (atau kesalahan ditangkap), Anda dapat mengirim tindakan untuk memeriksa apakah objek data Anda terisi penuh. Setelah sepenuhnya terisi, Anda dapat memperbarui loadingvariabel ke false, dan membuat Anda kondisionalGrid komponen .

Jadi misalnya:

// Component A

import { acceptData, catchError } from '../actions'

class ComponentA extends React.Component{

  componentDidMount () {

    fetch('yoururl.com/data')
      .then( response => response.json() )
      // send your data to the global state data array
      .then( data => this.props.acceptData(data, grandChildNumber) )
      .catch( error => this.props.catchError(error, grandChildNumber) )

    // make all your fetch calls here

  }

  // Conditionally render your Loading or Grid based on the global state variable 'loading'
  render() {
    return (
      { this.props.loading && <Loading /> }
      { !this.props.loading && <Grid /> }
    )
  }

}


const mapStateToProps = state => ({ loading: state.loading })

const mapDispatchToProps = dispatch => ({ 
  acceptData: data => dispatch( acceptData( data, number ) )
  catchError: error=> dispatch( catchError( error, number) )
})
// Grid - not much going on here...

render () {
  return (
    <div className="Grid">
      <GrandChild1 number={1} />
      <GrandChild2 number={2} />
      <GrandChild3 number={3} />
      ...
      // Or render the granchildren from an array with a .map, or something similar
    </div>
  )
}
// Grandchild

// Conditionally render either an error or your data, depending on what came back from fetch
render () {
  return (
    { !this.props.data[this.props.number].error && <Your Content Here /> }
    { this.props.data[this.props.number].error && <Your Error Here /> }
  )
}

const mapStateToProps = state => ({ data: state.data })

Peredam Anda akan memegang objek negara global yang akan mengatakan apakah semuanya siap untuk pergi atau belum:

// reducers.js

const initialState = {
  data: [{},{},{},{}...], // 9 empty objects
  loading: true
}

const reducers = (state = initialState, action) {
  switch(action.type){

    case RECIEVE_SOME_DATA:
      return {
        ...state,
        data: action.data
      }

     case RECIEVE_ERROR:
       return {
         ...state,
         data: action.data
       }

     case STOP_LOADING:
       return {
         ...state,
         loading: false
       }

  }
}

Dalam tindakan Anda:


export const acceptData = (data, number) => {
  // First revise your data array to have the new data in the right place
  const updatedData = data
  updatedData[number] = data
  // Now check to see if all your data objects are populated
  // and update your loading state:
  dispatch( checkAllData() )
  return {
    type: RECIEVE_SOME_DATA,
    data: updatedData,
  }
}

// error checking - because you want your stuff to render even if one of your api calls 
// catches an error
export const catchError(error, number) {
  // First revise your data array to have the error in the right place
  const updatedData = data
  updatedData[number].error = error
  // Now check to see if all your data objects are populated
  // and update your loading state:
  dispatch( checkAllData() )
  return {
    type: RECIEVE_ERROR,
    data: updatedData,
  }
}

export const checkAllData() {
  // Check that every data object has something in it
  if ( // fancy footwork to check each object in the data array and see if its empty or not
    store.getState().data.every( dataSet => 
      Object.entries(dataSet).length === 0 && dataSet.constructor === Object ) ) {
        return {
          type: STOP_LOADING
        }
      }
  }

Ke samping

Jika Anda benar-benar menikah dengan gagasan bahwa panggilan API Anda tinggal di dalam masing-masing cucu, tetapi bahwa seluruh Kotak cucu tidak memberikan hingga semua panggilan API selesai, Anda harus menggunakan solusi yang sama sekali berbeda. Dalam hal ini, cucu Anda harus diberikan dari awal untuk melakukan panggilan, tetapi memiliki kelas css display: none, yang hanya berubah setelah variabel keadaan global loadingditandai sebagai salah. Ini juga bisa dilakukan, tetapi semacam selain dari React.

Seth Lutske
sumber
1

Anda berpotensi memecahkan masalah ini menggunakan React Suspense.

Peringatannya adalah bahwa itu bukan ide yang baik untuk Menunda melewati pohon komponen yang melakukan render (yaitu: Jika komponen Anda memulai proses render, itu bukan ide yang baik untuk menunda komponen itu, menurut pengalaman saya), jadi mungkin ide yang lebih baik untuk memulai permintaan di komponen yang membuat sel. Sesuatu seperti ini:

export default function App() {
  const cells = React.useMemo(
    () =>
      ingredients.map((_, index) => {
        // This starts the fetch but *does not wait for it to finish*.
        return <Cell resource={fetchIngredient(index)} />;
      }),
    []
  );

  return (
    <div className="App">
      <Grid>{cells}</Grid>
    </div>
  );
}

Sekarang, cukup bagaimana Suspense berpasangan dengan Redux saya tidak yakin. Seluruh ide di balik versi Suspense (percobaan!) Ini adalah Anda memulai pengambilan langsung selama siklus render komponen induk dan meneruskan objek yang mewakili pengambilan kepada anak-anak. Ini mencegah Anda harus memiliki semacam objek Penghalang (yang Anda perlukan dalam pendekatan lain).

Saya akan mengatakan bahwa saya tidak berpikir menunggu sampai semuanya diambil untuk menampilkan sesuatu adalah pendekatan yang tepat karena UI akan selambat koneksi paling lambat atau mungkin tidak berfungsi sama sekali!

Berikut sisa kode yang hilang:

const ingredients = [
  "Potato",
  "Cabbage",
  "Beef",
  "Bok Choi",
  "Prawns",
  "Red Onion",
  "Apple",
  "Raisin",
  "Spinach"
];

function randomTimeout(ms) {
  return Math.ceil(Math.random(1) * ms);
}

function fetchIngredient(id) {
  const task = new Promise(resolve => {
    setTimeout(() => resolve(ingredients[id]), randomTimeout(5000));
  });

  return new Resource(task);
}

// This is a stripped down version of the Resource class displayed in the React Suspense docs. It doesn't handle errors (and probably should).
// Calling read() will throw a Promise and, after the first event loop tick at the earliest, will return the value. This is a synchronous-ish API,
// Making it easy to use in React's render loop (which will not let you return anything other than a React element).
class Resource {
  constructor(promise) {
    this.task = promise.then(value => {
      this.value = value;
      this.status = "success";
    });
  }

  read() {
    switch (this.status) {
      case "success":
        return this.value;

      default:
        throw this.task;
    }
  }
}

function Cell({ resource }) {
  const data = resource.read();
  return <td>{data}</td>;
}

function Grid({ children }) {
  return (
    // This suspense boundary will cause a Loading sign to be displayed if any of the children suspend (throw a Promise).
    // Because we only have the one suspense boundary covering all children (and thus Cells), the fallback will be rendered
    // as long as at least one request is in progress.
    // Thanks to this approach, the Grid component need not be aware of how many Cells there are.
    <React.Suspense fallback={<h1>Loading..</h1>}>
      <table>{children}</table>
    </React.Suspense>
  );
}

Dan kotak pasir: https://codesandbox.io/s/falling-dust-b8e7s

Dan Pantry
sumber
1
baca saja komentarnya .. Saya tidak percaya itu akan sepele untuk menunggu semua komponen yang akan diberikan di DOM - dan itu untuk alasan yang bagus. Tampaknya memang sangat berantakan. Apa yang ingin Anda capai?
Dan Pantry
1

Pertama-tama, siklus hidup dapat memiliki metode asink / menunggu.

Apa yang perlu kita ketahui componentDidMountdan componentWillMount,

componentWillMount disebut sebagai parent pertama kemudian child.
componentDidMount melakukan yang sebaliknya.

Dalam pertimbangan saya, cukup menerapkannya menggunakan siklus hidup normal akan cukup baik.

  • komponen anak
async componentDidMount() {
  await this.props.yourRequest();
  // Check if satisfied, if true, add flag to redux store or something,
  // or simply check the res stored in redux in parent leading no more method needed here
  await this.checkIfResGood();
}
  • komponen induk
// Initial state `loading: true`
componentDidUpdate() {
  this.checkIfResHaveBad(); // If all good, `loading: false`
}
...
{this.state.loading ? <CircularProgress /> : <YourComponent/>}

Kemajuan Circular Material-UI

Karena rendering ulang anak menyebabkan orang tua melakukan hal yang sama, menangkapnya didUpdateakan baik-baik saja.
Dan, karena dipanggil setelah rendering, halaman tidak boleh diubah setelah itu jika Anda mengatur fungsi cek sebagai permintaan Anda.

Kami menggunakan implementasi ini di prod kami yang memiliki api menyebabkan 30-an untuk mendapatkan respon, semua bekerja dengan baik sejauh yang saya lihat.

masukkan deskripsi gambar di sini

keikai
sumber
0

Ketika Anda membuat panggilan api componentDidMount, ketika panggilan API akan menyelesaikan Anda akan memiliki data componentDidUpdatekarena siklus hidup ini akan melewati Anda prevPropsdan prevState. Jadi Anda dapat melakukan sesuatu seperti di bawah ini:

class Parent extends Component {
  getChildUpdateStatus = (data) => {
 // data is any data you want to send from child
}
render () {
 <Child sendChildUpdateStatus={getChildUpdateStatus}/>
}
}

class Child extends Component {
componentDidUpdate = (prevProps, prevState) => {
   //compare prevProps from this.props or prevState from current state as per your requirement
  this.props.sendChildUpdateStatus();
}

render () { 
   return <h2>{.. child rendering}</h2>
  }
}

Jika Anda ingin mengetahui hal ini sebelum komponen dirender kembali, Anda dapat menggunakannya getSnapshotBeforeUpdate .

https://reactjs.org/docs/react-component.html#getsnapshotbeforeupdate .

Pranay Tripathi
sumber
0

Ada beberapa pendekatan yang bisa Anda gunakan:

  1. Cara termudah adalah dengan mengirimkan panggilan balik ke komponen anak. Komponen anak ini kemudian dapat memanggil panggilan balik ini segera setelah mereka diberikan setelah mengambil data masing-masing atau logika bisnis lain yang ingin Anda masukkan. Berikut adalah kotak pasir untuk hal yang sama: https://codesandbox.io/s/unruffled-shockley-yf3f3
  2. Minta komponen dependen / anak untuk memperbarui ketika selesai dengan memberikan data yang diambil ke status aplikasi global yang akan didengarkan komponen Anda
  3. Anda juga dapat menggunakan React.useContext untuk membuat status terlokalisasi untuk komponen induk ini. Komponen anak kemudian dapat memperbarui konteks ini ketika selesai dengan rendering data yang diambil. Sekali lagi, komponen induk akan mendengarkan konteks ini dan dapat bertindak sesuai
Ravi Chaudhary
sumber