Reagieren - Wie erkennt man, wenn alle Unterkomponenten einer übergeordneten Komponente für den Benutzer sichtbar sind?

11

TL; DR

Wie kann eine übergeordnete Komponente wissen, wann das Rendern jeder untergeordneten Komponente abgeschlossen ist und das DOM für den Benutzer mit seiner aktuellsten Version sichtbar ist?

Angenommen, ich habe eine component Auntergeordnete GridKomponente, die aus 3x3Enkelkomponenten besteht. Jede dieser Enkelkomponenten ruft Daten von einem erholsamen API-Endpunkt ab und rendert sich selbst, wenn die Daten verfügbar werden.

Ich möchte den gesamten Bereich von Component Amit einem loaderPlatzhalter abdecken, der erst dann angezeigt wird, wenn die letzte Komponente im Raster die Daten erfolgreich abgerufen und gerendert hat, sodass sie sich bereits im DOM befinden und angezeigt werden können.

Die Benutzererfahrung sollte ein super reibungsloser Übergang vom "Lader" zu einem vollständig bestückten Raster sein, ohne zu flackern.

Mein Problem ist, genau zu wissen, wann die Komponenten unter dem Lader enthüllt werden müssen.

Gibt es einen Mechanismus, auf den ich mich verlassen kann, um dies mit absoluter Genauigkeit zu tun? Ich muss kein Zeitlimit für den Loader fest codieren. Soweit ich ComponentDidMountweiß, ist es auch unzuverlässig , sich auf jedes Kind zu verlassen, da dies nicht garantiert, dass die Komponente zum Zeitpunkt des Anrufs für den Benutzer vollständig sichtbar ist.

Um die Frage noch weiter zu vertiefen:

Ich habe eine Komponente, die Daten rendert. Nachdem es initialisiert wurde, hat es es nicht, also componentDidMounttrifft es einen API-Endpunkt dafür. Sobald die Daten empfangen wurden, ändert sich ihr Status, um sie wiederzugeben. Dies führt verständlicherweise zu einem erneuten Rendern des Endzustands dieser Komponente. Meine Frage lautet: Woher weiß ich, wann dieses erneute Rendern stattgefunden hat und sich im DOM des Benutzers widerspiegelt ? Dieser Zeitpunkt! = Der Zeitpunkt, zu dem sich der Status der Komponente geändert hat, um Daten zu enthalten.

JasonGenX
quelle
Wie verwaltet der Staat? Wie mit Redux? oder ist es nur mit den Komponenten?
TechTurtle
Es befindet sich innerhalb der Komponenten, kann jedoch externalisiert werden. Ich benutze Redux für andere Dinge.
JasonGenX
und woher wissen Sie, dass die letzte Komponente im Raster das Abrufen von Daten abgeschlossen hat?
TechTurtle
Ich nicht. Die Gitterkomponenten sind sich nicht bewusst. In allen Unterkomponenten verwende ComponentDidMountich axioszum Abrufen von Daten. Wenn die Daten eingehen, ändere ich den Status dieser Komponente, wodurch die Daten gerendert werden. Theoretisch können 8
untergeordnete
1
Ich kann das tun, aber woher weiß ich, wann ich das Loader-Overlay enthüllen muss, damit Benutzer nie sehen, wie eine Komponente von "leer" zu "voll" wechselt? Dies ist keine Frage zum Abrufen von Daten. Es geht ums Rendern ... auch wenn ich nur eine untergeordnete Komponente hatte. ComponentDidMount ist nicht genug. Ich muss wissen, wann das Rendern nach dem Abrufen der Daten abgeschlossen ist und das DOM vollständig aktualisiert ist, damit ich das Loader-Overlay enthüllen kann.
JasonGenX

Antworten:

5

In React gibt es zwei Lifecycle-Hooks, die aufgerufen werden, nachdem das DOM einer Komponente gerendert wurde:

Für Ihren Anwendungsfall ist Ihre übergeordnete Komponente P interessiert, wenn N untergeordnete Komponenten jeweils eine Bedingung X erfüllt haben . X kann als Sequenz definiert werden:

  • asynchrone Operation abgeschlossen
  • Komponente wurde gerendert

Durch Kombinieren des Status der Komponente und Verwenden des componentDidUpdateHooks können Sie feststellen, wann die Sequenz abgeschlossen ist und Ihre Komponente die Bedingung X erfüllt.

Sie können verfolgen, wann Ihre asynchrone Operation abgeschlossen ist, indem Sie eine Statusvariable festlegen. Zum Beispiel:

this.setState({isFetched: true})

Nach dem Einstellen des Status ruft React die componentDidUpdateFunktion Ihrer Komponenten auf . Durch Vergleichen der aktuellen und vorherigen Statusobjekte in dieser Funktion können Sie der übergeordneten Komponente signalisieren, dass Ihre asynchrone Operation abgeschlossen und der Status Ihrer neuen Komponente gerendert wurde:

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

In Ihrer P-Komponente können Sie einen Zähler verwenden, um zu verfolgen, wie viele Kinder sinnvoll aktualisiert wurden:

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

Wenn Sie die Länge von N kennen , können Sie schließlich wissen, wann alle sinnvollen Aktualisierungen stattgefunden haben, und in Ihrer Rendermethode von P entsprechend handeln :

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

Ich würde es so einrichten, dass Sie sich auf eine globale Statusvariable verlassen, um Ihren Komponenten mitzuteilen, wann sie gerendert werden sollen. Redux ist besser für dieses Szenario, in dem viele Komponenten miteinander sprechen, und Sie haben in einem Kommentar erwähnt, dass Sie es manchmal verwenden. Also skizziere ich eine Antwort mit Redux.

Sie müssten Ihre API-Aufrufe in den übergeordneten Container verschieben. Component A . Wenn Sie möchten, dass Ihre Enkelkinder erst nach Abschluss der API-Aufrufe gerendert werden, können Sie diese API-Aufrufe nicht in den Enkelkindern selbst behalten. Wie kann ein API-Aufruf von einer Komponente aus erfolgen, die noch nicht vorhanden ist?

Sobald alle API-Aufrufe ausgeführt wurden, können Sie mithilfe von Aktionen eine globale Statusvariable aktualisieren, die eine Reihe von Datenobjekten enthält. Jedes Mal, wenn Daten empfangen werden (oder ein Fehler abgefangen wird), können Sie eine Aktion auslösen, um zu überprüfen, ob Ihr Datenobjekt vollständig ausgefüllt ist. Sobald es vollständig ausgefüllt ist, können Sie eine loadingVariable auf aktualisieren falseund Ihre unter bestimmten Bedingungen rendernGrid Komponente .

Also zum Beispiel:

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

Ihr Reduzierer hält das globale Statusobjekt bereit, das angibt, ob alles bereit ist oder nicht:

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

  }
}

In Ihren Handlungen:


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

Beiseite

Wenn Sie wirklich mit der Idee verheiratet sind, dass Ihre API-Aufrufe in jedem Enkelkind gespeichert sind, das gesamte Raster der Enkelkinder jedoch erst nach Abschluss aller API-Aufrufe gerendert wird, müssen Sie eine völlig andere Lösung verwenden. In diesem Fall müssten Ihre Enkelkinder von Anfang an gerendert werden, um ihre Aufrufe zu tätigen, haben jedoch eine CSS-Klasse mit display: none, die sich erst ändert, nachdem die globale Statusvariable loadingals falsch markiert wurde. Dies ist auch machbar, aber irgendwie neben dem Punkt der Reaktion.

Seth Lutske
quelle
1

Sie können dies möglicherweise mit React Suspense lösen.

Die Einschränkung ist, dass es keine gute Idee ist, über den Komponentenbaum hinaus anzuhalten, der das Rendern ausführt (dh: Wenn Ihre Komponente einen Renderprozess startet, ist es meiner Erfahrung nach keine gute Idee, diese Komponente anzuhalten) Wahrscheinlich ist es eine bessere Idee, die Anforderungen in der Komponente zu starten, die die Zellen rendert . Etwas wie das:

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

Wie Suspense sich mit Redux paart, weiß ich nicht genau. Die ganze Idee hinter dieser (experimentellen!) Version von Suspense ist, dass Sie den Abruf sofort während des Renderzyklus einer übergeordneten Komponente starten und ein Objekt übergeben, das einen Abruf an die untergeordneten Komponenten darstellt. Dies verhindert, dass Sie eine Art Barrier-Objekt benötigen (das Sie bei anderen Ansätzen benötigen würden).

Ich werde sagen, dass ich nicht denke, dass es der richtige Ansatz ist , zu warten, bis alles abgerufen wurde, um etwas anzuzeigen , da dann die Benutzeroberfläche so langsam ist wie die langsamste Verbindung oder möglicherweise überhaupt nicht funktioniert!

Hier ist der Rest des fehlenden Codes:

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

Und eine Sandbox: https://codesandbox.io/s/falling-dust-b8e7s

Dan Pantry
quelle
1
Lesen Sie einfach die Kommentare. Ich glaube nicht, dass es trivial wäre, darauf zu warten, dass alle Komponenten im DOM gerendert werden - und das aus gutem Grund. Das scheint in der Tat sehr hackig zu sein. Was versuchst du zu erreichen?
Dan Pantry
1

Erstens könnte der Lebenszyklus eine Async / Wait-Methode haben.

Was wir wissen müssen componentDidMountund componentWillMount,

componentWillMount heißt zuerst übergeordnet, dann untergeordnet.
componentDidMount macht das Gegenteil.

Meiner Meinung nach wäre es gut genug, sie einfach mit einem normalen Lebenszyklus zu implementieren.

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

Material-UI CircularProgress

Da das erneute Rendern des Kindes dazu führt, dass die Eltern dasselbe tun, ist es in didUpdateOrdnung , es einzufangen .
Und da es nach dem Rendern aufgerufen wird, sollten die Seiten danach nicht mehr geändert werden, wenn Sie die Überprüfungsfunktion als Ihre Anforderung festlegen.

Wir verwenden diese Implementierung in unserem Produkt, das eine API hat, die 30s veranlasst, die Antwort zu erhalten. Soweit ich sehe, hat alles gut funktioniert.

Geben Sie hier die Bildbeschreibung ein

Keikai
quelle
0

Während Sie einen componentDidMountAPI-Aufruf tätigen, werden die Daten beim Auflösen des API-Aufrufs gespeichert, componentDidUpdatewährend dieser Lebenszyklus Sie prevPropsund prevState. Sie können also Folgendes tun:

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

Wenn Sie dies wissen möchten, bevor die Komponente erneut gerendert wird, können Sie verwenden getSnapshotBeforeUpdate.

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

Pranay Tripathi
quelle
0

Es gibt mehrere Ansätze, mit denen Sie gehen können:

  1. Am einfachsten ist es, einen Rückruf an die untergeordnete Komponente zu übergeben. Diese untergeordneten Komponenten können diesen Rückruf dann aufrufen, sobald sie nach dem Abrufen ihrer einzelnen Daten oder einer anderen Geschäftslogik, die Sie einfügen möchten, gerendert wurden. Hier ist eine Sandbox für dasselbe: https://codesandbox.io/s/unruffled-shockley-yf3f3
  2. Bitten Sie die abhängigen / untergeordneten Komponenten, zu aktualisieren, wenn sie mit dem Rendern der abgerufenen Daten in einen globalen App-Status fertig sind, den Ihre Komponente abhören würde
  3. Sie können auch React.useContext verwenden, um einen lokalisierten Status für diese übergeordnete Komponente zu erstellen. Untergeordnete Komponenten können diesen Kontext dann aktualisieren, wenn sie mit dem Rendern der abgerufenen Daten fertig sind. Auch hier würde die übergeordnete Komponente diesen Kontext abhören und kann entsprechend handeln
Ravi Chaudhary
quelle