Reagisci - Come rilevare quando tutti i sottocomponenti di un componente padre sono visibili all'utente?


11

TL; DR

Come può un componente genitore sapere quando il rendering di ogni componente figlio è terminato e il DOM visibile all'utente con la sua versione più aggiornata?

Diciamo che ho un componente component Ache ha un Gridcomponente figlio costituito da 3x3componenti nipote. Ognuno di questi componenti del nipote recupera i dati da un endpoint API riposante e si rende automaticamente quando i dati diventano disponibili.

Vorrei coprire l'intera area di Component Acon un loadersegnaposto, per essere svelato solo quando l'ultimo dei componenti nella griglia ha recuperato i dati con successo e li ha resi, in modo che siano già sul DOM e possano essere visualizzati.

L'esperienza utente dovrebbe essere una transizione senza intoppi dal "caricatore" a una griglia completamente popolata senza sfarfallio.

Il mio problema è sapere esattamente quando svelare i componenti sotto il caricatore.

Esiste un meccanismo su cui posso fare affidamento per farlo con assoluta precisione? Non scrivo un limite di tempo per il caricatore. Come ho capito, fare affidamento su ComponentDidMountogni bambino è anche inaffidabile in quanto in realtà non garantisce che il componente sia completamente visibile all'utente al momento della chiamata.

Per distillare ulteriormente la domanda:

Ho un componente che rende alcuni tipi di dati. Dopo che è stato inizializzato, non lo ha, quindi componentDidMountraggiunge un endpoint API per esso. Una volta che riceve i dati, sta cambiando il suo stato per riflettere. Ciò comprensibilmente provoca un nuovo rendering dello stato finale di quel componente. La mia domanda è questa: come faccio a sapere quando è stato eseguito quel nuovo rendering e si riflette sull'utente che si trova di fronte a DOM. Quel punto nel tempo! = Il momento in cui lo stato del componente è cambiato per contenere i dati.


Come stanno gestendo lo stato? Ti piace usare Redux? o è puramente all'interno dei componenti?
TechTurtle

È all'interno dei componenti, ma può essere esternalizzato. Uso Redux per altre cose.
JasonGenX,

e come fai a sapere che l'ultimo componente della griglia ha terminato il recupero dei dati?
TechTurtle,

Io non. I componenti della griglia non sono reciprocamente consapevoli. In ogni sottocomponente ComponentDidMountche sto usando axiosper recuperare i dati. quando arrivano i dati, cambio lo stato di quel componente che causa un rendering dei dati. In teoria, 8 componenti figlio possono essere recuperati entro 3 secondi e l'ultimo impiegherebbe 15 secondi ...
JasonGenX

1
Posso farlo, ma come faccio a sapere quando svelare la sovrapposizione del caricatore in modo tale che gli utenti non vedranno mai come un componente passa da "vuoto" a "pieno"? Questa non è una domanda sul recupero dei dati. Riguarda il rendering ... anche se avessi un solo componente figlio. ComponentDidMount non è abbastanza. Devo sapere quando il post-data-fetching-render è stato completato e DOM è stato completamente aggiornato in modo da poter svelare l'overlay del caricatore.
JasonGenX,

Risposte:


5

Ci sono due hook del ciclo di vita in React che vengono chiamati dopo il rendering del DOM di un componente:

Per il vostro caso d'uso di un genitore componente P è interessato quando N componenti figlio hanno ciascuna soddisfatto alcune condizioni X . X può essere definita come una sequenza:

  • operazione asincrona completata
  • il componente ha eseguito il rendering

Combinando lo stato del componente e utilizzando l' componentDidUpdatehook, è possibile sapere quando la sequenza è stata completata e il componente soddisfa la condizione X.

Puoi tenere traccia di quando l'operazione asincrona è stata completata impostando una variabile di stato. Per esempio:

this.setState({isFetched: true})

Dopo aver impostato lo stato, React chiamerà la componentDidUpdatefunzione dei componenti . Confrontando gli oggetti di stato corrente e precedente all'interno di questa funzione, è possibile segnalare al componente padre che l'operazione asincrona è stata completata e che lo stato del nuovo componente è stato visualizzato:

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

Nel tuo componente P, puoi usare un contatore per tenere traccia di quanti bambini hanno aggiornato in modo significativo:

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

Infine, conoscendo la lunghezza di N puoi sapere quando si sono verificati tutti gli aggiornamenti significativi e agire di conseguenza nel tuo metodo di rendering di P :

const childRenderingFinished = this.state.counter >= N

1

Lo configurerei in modo da fare affidamento su una variabile di stato globale per indicare ai componenti quando eseguire il rendering. Redux è meglio per questo scenario in cui molti componenti parlano tra loro e in un commento hai menzionato che lo usi a volte. Quindi disegnerò una risposta usando Redux.

Avresti per spostare le chiamate API al contenitore principale, Component A. Se desideri che i tuoi nipoti vengano visualizzati solo dopo che le chiamate API sono state completate, non puoi mantenere quelle chiamate API nei nipoti stessi. Come è possibile effettuare una chiamata API da un componente che non esiste ancora?

Dopo aver effettuato tutte le chiamate API, è possibile utilizzare le azioni per aggiornare una variabile di stato globale contenente un gruppo di oggetti dati. Ogni volta che i dati vengono ricevuti (o viene rilevato un errore), è possibile inviare un'azione per verificare se l'oggetto dati è completamente compilato. Una volta compilato completamente, è possibile aggiornare una loadingvariabile falsee rendere condizionatamente il Gridcomponente.

Quindi per esempio:

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

Il tuo riduttore tratterrà l'oggetto dello stato globale che dirà se le cose sono ancora pronte per partire:

// 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
       }

  }
}

Nelle tue azioni:


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

A parte

Se sei davvero sposato all'idea che le tue chiamate API vivano all'interno di ciascun nipote, ma che l'intera griglia dei nipoti non venga visualizzata fino a quando tutte le chiamate API non sono state completate, dovresti utilizzare una soluzione completamente diversa. In questo caso, i tuoi nipoti dovrebbero essere resi dall'inizio per effettuare le loro chiamate, ma avere una classe CSS con display: none, che cambia solo dopo che la variabile di stato globale loadingè contrassegnata come falsa. Anche questo è fattibile, ma in qualche modo oltre al punto di React.


1

Potresti potenzialmente risolverlo usando React Suspense.

L'avvertenza è che non è una buona idea sospendere oltre l'albero dei componenti che esegue il rendering (vale a dire: se il componente avvia un processo di rendering, non è una buona idea far sospendere quel componente, nella mia esperienza), quindi è probabilmente un'idea migliore per dare il via alle richieste nel componente che rende le celle. Qualcosa come questo:

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

Ora, non so come Suspense si accoppi con Redux. L'idea alla base di questa versione (sperimentale!) Di Suspense è di avviare immediatamente il recupero durante il ciclo di rendering di un componente genitore e passare un oggetto che rappresenta un recupero ai bambini. Questo ti impedisce di avere un qualche tipo di oggetto Barrier (di cui avresti bisogno in altri approcci).

Dirò che non penso che aspettare fino a quando tutto è stato recuperato per visualizzare qualcosa sia l'approccio giusto perché l'interfaccia utente sarà lenta quanto la connessione più lenta o potrebbe non funzionare affatto!

Ecco il resto del codice mancante:

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

E una sandbox: https://codesandbox.io/s/falling-dust-b8e7s


1
basta leggere i commenti .. Non credo che sarebbe banale aspettare che tutti i componenti vengano resi nel DOM - e questo è per una buona ragione. Sembra davvero molto confuso. Cosa stai cercando di ottenere?
Dan Pantry,

1

Prima di tutto, il ciclo di vita potrebbe avere un metodo asincrono / attendere.

Cosa dobbiamo sapere componentDidMounte componentWillMount,

componentWillMount è chiamato primo genitore quindi figlio.
componentDidMount fa il contrario.

A mio avviso, implementarli semplicemente usando il normale ciclo di vita sarebbe abbastanza buono.

  • componente figlio
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();
}
  • componente principale
// Initial state `loading: true`
componentDidUpdate() {
  this.checkIfResHaveBad(); // If all good, `loading: false`
}
...
{this.state.loading ? <CircularProgress /> : <YourComponent/>}

Material-UI CircularProgress

Dato che il rendering secondario del figlio fa sì che il genitore faccia lo stesso, catturarlo didUpdateandrebbe bene.
E, dal momento che viene chiamato dopo il rendering, le pagine non dovrebbero essere modificate successivamente se si imposta la funzione di controllo come richiesta.

Usiamo questa implementazione nel nostro prodotto che ha un api che causa 30s per ottenere la risposta, tutto ha funzionato bene per quanto vedo.

inserisci qui la descrizione dell'immagine


0

Mentre effettui una chiamata API componentDidMount, quando la chiamata API si risolverà avrai i dati componentDidUpdatementre questo ciclo di vita ti passerà prevPropse prevState. Quindi puoi fare qualcosa come sotto:

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

Se vuoi sapere questo prima che il componente venga visualizzato nuovamente, puoi usarlo getSnapshotBeforeUpdate.

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


0

Esistono diversi approcci che puoi seguire:

  1. Il modo più semplice è passare un callback al componente figlio. Questi componenti figlio possono quindi richiamare questo callback non appena hanno eseguito il rendering dopo aver recuperato i loro dati individuali o qualsiasi altra logica aziendale che si desidera mettere. Ecco una sandbox per lo stesso: https://codesandbox.io/s/unruffled-shockley-yf3f3
  2. Chiedere ai componenti figlio / dipendente di aggiornarsi al termine del rendering dei dati recuperati in uno stato di app globale che il componente ascolterebbe
  3. Puoi anche utilizzare React.useContext per creare uno stato localizzato per questo componente padre. I componenti figlio possono quindi aggiornare questo contesto al termine del rendering dei dati recuperati. Ancora una volta, il componente genitore ascolta questo contesto e può agire di conseguenza
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.