React - Comment détecter quand tous les sous-composants d'un composant parent sont visibles pour l'utilisateur?

11

TL; DR

Comment un composant parent peut-il savoir quand le rendu de chaque composant enfant en dessous est terminé et le DOM visible par l'utilisateur avec sa version la plus récente?

Disons que j'en ai un component Aqui a un Gridcomposant enfant composé de 3x3composants petits - enfants. Chacun de ces composants de petit-enfant récupère les données à partir d'un point de terminaison API reposant et s'affiche lorsque les données deviennent disponibles.

Je voudrais couvrir toute la zone Component Aavec un loaderespace réservé, pour être dévoilé uniquement lorsque le dernier des composants de la grille a récupéré les données avec succès et les a rendues, de sorte qu'elles sont déjà sur le DOM et peuvent être consultées.

L'expérience utilisateur doit être une transition super fluide du «chargeur» vers une grille entièrement peuplée sans scintillement.

Mon problème est de savoir exactement quand dévoiler les composants sous le chargeur.

Existe-t-il un mécanisme sur lequel je peux compter pour le faire avec une précision absolue? Je n'ai pas à coder en dur une limite de temps pour le chargeur. Si je comprends bien, compter sur ComponentDidMountchaque enfant n'est pas non plus fiable, car cela ne garantit pas réellement que le composant est entièrement visible pour l'utilisateur au moment de l'appel.

Pour distiller encore la question:

J'ai un composant qui rend une sorte de données. Une fois qu'il est initialisé, il ne l'a pas, donc dans son componentDidMountil frappe un point de terminaison API pour lui. Une fois qu'il reçoit les données, il change son état pour les refléter. Cela provoque naturellement un nouveau rendu de l'état final de ce composant. Ma question est la suivante: comment savoir quand ce nouveau rendu a eu lieu et se reflète dans le DOM utilisateur. Ce point dans le temps! = Le moment où l'état du composant a changé pour contenir des données.

JasonGenX
la source
Comment gérez-vous l'État? Comme utiliser Redux? ou est-ce purement avec les composants?
TechTurtle
Il fait partie des composants, mais peut être externalisé. J'utilise Redux pour d'autres choses.
JasonGenX
et comment savez-vous que le dernier composant de la grille a fini de récupérer les données?
TechTurtle
Je ne. Les composants de la grille ne se connaissent pas. Dans tous les sous-composants que ComponentDidMountj'utilise axiospour récupérer des données. lorsque les données arrivent, je change l'état de ce composant qui provoque un rendu des données. En théorie, 8 composants enfants peuvent être récupérés en 3 secondes, et le dernier prendrait 15 secondes ...
JasonGenX
1
Je peux le faire, mais comment savoir quand dévoiler la superposition du chargeur de telle sorte que les utilisateurs ne verront jamais comment un composant passe de "vide" à "plein"? Ce n'est pas une question sur la récupération de données. Il s'agit du rendu ... même si je n'avais qu'un seul composant enfant. ComponentDidMount n'est pas suffisant. J'ai besoin de savoir quand le rendu post-extraction de données est terminé et que DOM est entièrement mis à jour pour que je puisse dévoiler la superposition du chargeur.
JasonGenX

Réponses:

5

Il y a deux hooks de cycle de vie dans React qui sont appelés après le rendu DOM d'un composant:

Pour votre cas d' utilisation de votre composant parent P est intéressé lorsque N composants enfants ont chacun satisfait une condition X . X peut être défini comme une séquence:

  • opération asynchrone terminée
  • le composant a rendu

En combinant l'état du composant et en utilisant le componentDidUpdatecrochet, vous pouvez savoir quand la séquence est terminée et que votre composant remplit la condition X.

Vous pouvez garder une trace de la fin de votre opération asynchrone en définissant une variable d'état. Par exemple:

this.setState({isFetched: true})

Après avoir défini l'état, React appellera la componentDidUpdatefonction de vos composants . En comparant les objets d'état actuel et précédent dans cette fonction, vous pouvez signaler au composant parent que votre opération asynchrone s'est terminée et que l'état de votre nouveau composant a été rendu:

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

Dans votre composant P, vous pouvez utiliser un compteur pour garder une trace du nombre d'enfants mis à jour de manière significative:

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

Enfin, en connaissant la longueur de N, vous pouvez savoir quand toutes les mises à jour significatives se sont produites et agir en conséquence dans votre méthode de rendu de P :

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

Je le configurerais pour que vous vous appuyiez sur une variable d'état globale pour indiquer à vos composants quand effectuer le rendu. Redux est meilleur pour ce scénario où de nombreux composants se parlent et vous avez mentionné dans un commentaire que vous l'utilisez parfois. Je vais donc esquisser une réponse en utilisant Redux.

Il faudrait passer vos appels API au conteneur parent, Component A. Si vous souhaitez que vos petits-enfants ne s'affichent qu'après la fin des appels d'API, vous ne pouvez pas conserver ces appels d'API dans les petits-enfants eux-mêmes. Comment faire un appel d'API à partir d'un composant qui n'existe pas encore?

Une fois tous les appels d'API effectués, vous pouvez utiliser des actions pour mettre à jour une variable d'état globale contenant un groupe d'objets de données. Chaque fois que des données sont reçues (ou qu'une erreur est détectée), vous pouvez envoyer une action pour vérifier si votre objet de données est entièrement rempli. Une fois complètement remplie, vous pouvez mettre à jour une loadingvariable falseet rendre votre Gridcomposant sous condition .

Ainsi, par exemple:

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

Votre réducteur contiendra l'objet d'état global qui dira si tout est prêt ou non:

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

  }
}

Dans vos actions:


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

De côté

Si vous êtes vraiment marié à l'idée que vos appels d'API vivent à l'intérieur de chaque petit-enfant, mais que l'ensemble de la grille des petits-enfants ne s'affiche pas tant que tous les appels d'API ne sont pas terminés, vous devrez utiliser une solution complètement différente. Dans ce cas, vos petits-enfants devraient être restitués dès le début pour effectuer leurs appels, mais avec une classe css avec display: none, qui ne change qu'après que la variable d'état global loadingest marquée comme fausse. C'est également faisable, mais en plus du point de React.

Seth Lutske
la source
1

Vous pouvez potentiellement résoudre ce problème en utilisant React Suspense.

La mise en garde est que ce n'est pas une bonne idée de suspendre au-delà de l'arborescence des composants qui effectue le rendu (c'est-à-dire: si votre composant lance un processus de rendu, ce n'est pas une bonne idée de suspendre ce composant, d'après mon expérience), c'est donc probablement une meilleure idée pour lancer les requêtes dans le composant qui rend les cellules. Quelque chose comme ça:

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

Maintenant, tout à fait comment Suspense s'associe avec Redux, je ne suis pas sûr. L'idée derrière cette version (expérimentale!) De Suspense est que vous démarrez l'extraction immédiatement pendant le cycle de rendu d'un composant parent et passez un objet qui représente une extraction aux enfants. Cela vous évite d'avoir à avoir une sorte d'objet Barrière (dont vous auriez besoin dans d'autres approches).

Je dirai que je ne pense pas qu'attendre que tout soit allé pour afficher quoi que ce soit soit la bonne approche car alors l'interface utilisateur sera aussi lente que la connexion la plus lente ou peut ne pas fonctionner du tout!

Voici le reste du code manquant:

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

Et un bac à sable: https://codesandbox.io/s/falling-dust-b8e7s

Dan Pantry
la source
1
il suffit de lire les commentaires .. Je ne pense pas qu'il serait trivial d'attendre que tous les composants soient rendus dans le DOM - et c'est pour une bonne raison. Cela semble vraiment très hacky. Qu'essayez-vous de réaliser?
Dan Pantry
1

Tout d'abord, le cycle de vie pourrait avoir une méthode asynchrone / attente.

Ce que nous devons savoir componentDidMountet componentWillMount,

componentWillMount est appelé premier parent puis enfant.
componentDidMount fait le contraire.

À mon avis, il suffit de les mettre en œuvre en utilisant un cycle de vie normal.

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

Material-UI CircularProgress

Étant donné que le rendu de l'enfant oblige le parent à faire de même, l'attraper didUpdateserait bien.
Et, comme il est appelé après le rendu, les pages ne doivent pas être modifiées après cela si vous définissez la fonction de vérification comme votre demande.

Nous utilisons cette implémentation dans notre prod qui a une api provoquant 30s pour obtenir la réponse, tout fonctionnait bien pour autant que je vois.

entrez la description de l'image ici

keikai
la source
0

Lorsque vous effectuez un appel componentDidMountAPI, lorsque l'appel API sera résolu, vous aurez les données à componentDidUpdatemesure que ce cycle de vie vous passera prevPropset prevState. Vous pouvez donc faire quelque chose comme ci-dessous:

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

Si vous voulez le savoir avant le rendu du composant, vous pouvez l'utiliser getSnapshotBeforeUpdate.

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

Pranay Tripathi
la source
0

Il existe plusieurs approches avec lesquelles vous pouvez opter:

  1. Le moyen le plus simple consiste à passer un rappel au composant enfant. Ces composants enfants peuvent ensuite appeler ce rappel dès qu'ils ont rendu après avoir récupéré leurs données individuelles ou toute autre logique métier que vous souhaitez mettre. Voici un sandbox pour le même: https://codesandbox.io/s/unruffled-shockley-yf3f3
  2. Demander aux composants dépendants / enfants de se mettre à jour lorsqu'ils ont terminé de rendre les données extraites dans un état d'application global que votre composant écouterait
  3. Vous pouvez également utiliser React.useContext pour créer un état localisé pour ce composant parent. Les composants enfants peuvent ensuite mettre à jour ce contexte lorsqu'ils ont terminé le rendu des données extraites. Encore une fois, la composante parent serait à l'écoute de ce contexte et peut agir en conséquence
Ravi Chaudhary
la source