Reaccionar: ¿cómo detectar cuándo todos los subcomponentes de un componente principal son visibles para el usuario?


11

TL; DR

¿Cómo puede un componente principal saber cuándo ha finalizado la representación de cada componente secundario y el DOM visible para el usuario con su versión más actualizada?

Digamos que tengo un componente component Asecundario Gridque consta de 3x3componentes de nieto. Cada uno de esos componentes nietos obtiene datos de un punto final de API tranquilo y se representa cuando los datos están disponibles.

Me gustaría cubrir toda el área Component Acon un loadermarcador de posición, que se revelará solo cuando el último de los componentes en la cuadrícula haya obtenido los datos con éxito y los haya procesado, de modo que ya esté en el DOM y se pueda ver.

La experiencia del usuario debe ser una transición súper suave de "cargador" a una grilla completamente poblada sin parpadeo.

Mi problema es saber exactamente cuándo revelar los componentes debajo del cargador.

¿Hay algún mecanismo en el que pueda confiar para hacer esto con absoluta precisión? No codifico un límite de tiempo para el cargador. Según tengo entendido, confiar en ComponentDidMountcada niño tampoco es confiable, ya que en realidad no garantiza que el componente sea completamente visible para el usuario en el momento de la llamada.

Para destilar la pregunta aún más:

Tengo un componente que representa algún tipo de datos. Después de que se inicializa, no lo tiene, por lo componentDidMountque llega a un punto final API para él. Una vez que recibe los datos, está cambiando su estado para reflejarlos. Es comprensible que esto provoque una nueva representación del estado final de ese componente. Mi pregunta es la siguiente: ¿Cómo sé cuando que volver a hacer ha tenido lugar y se refleja en el DOM frente del usuario. Ese punto en el tiempo! = El punto en el tiempo cuando el estado del componente ha cambiado para contener datos.


¿Cómo se gestiona el estado? ¿Te gusta usar Redux? o es puramente dentro de los componentes?
TechTurtle

Está dentro de los componentes, pero se puede externalizar. Yo uso Redux para otras cosas.
JasonGenX

¿Y cómo sabe que el último componente de la cuadrícula ha terminado de recuperar datos?
TechTurtle

Yo no. Los componentes de la cuadrícula no se conocen entre sí. En cada subcomponente ComponentDidMountque estoy usando axiospara obtener datos. cuando llegan los datos, cambio el estado de ese componente que causa una representación de los datos. En teoría, 8 componentes secundarios pueden obtener en 3 segundos, y el último tomaría 15 segundos ...
JasonGenX

1
Puedo hacerlo, pero ¿cómo sé cuándo desvelar la superposición del cargador para que los usuarios nunca vean cómo un componente pasa de "vacío" a "lleno"? Esta no es una pregunta sobre la obtención de datos. Se trata de renderizar ... incluso si solo tuviera un componente hijo. ComponentDidMount no es suficiente. Necesito saber cuándo se ha completado el procesamiento posterior a la obtención de datos y si DOM está completamente actualizado para poder revelar la superposición del cargador.
JasonGenX

Respuestas:


5

Hay dos ganchos de ciclo de vida en React que se invocan después de que se ha procesado el DOM de un componente:

Para su caso de uso de sus padres componente P está interesado en N componentes hijos tienen cada uno satisfecho alguna condición X . X se puede definir como una secuencia:

  • operación asincrónica completada
  • componente ha prestado

Al combinar el estado del componente y usar el componentDidUpdategancho, puede saber cuándo se ha completado la secuencia y si su componente cumple la condición X.

Puede realizar un seguimiento de cuándo se ha completado su operación asincrónica configurando una variable de estado. Por ejemplo:

this.setState({isFetched: true})

Después de configurar el estado, React llamará a la componentDidUpdatefunción de sus componentes . Al comparar los objetos de estado actuales y anteriores dentro de esta función, puede indicarle al componente principal que su operación asíncrona se ha completado y que el estado de su nuevo componente ha generado:

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

En su componente P, puede usar un contador para realizar un seguimiento de cuántos niños han actualizado de manera significativa:

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

Finalmente, al conocer la longitud de N , puede saber cuándo se han producido todas las actualizaciones significativas y actuar en consecuencia en su método de representación de P :

const childRenderingFinished = this.state.counter >= N

1

Lo configuraría para que confíe en una variable de estado global para indicar a sus componentes cuándo renderizar. Redux es mejor para este escenario en el que muchos componentes están hablando entre sí, y usted mencionó en un comentario que a veces lo usa. Entonces esbozaré una respuesta usando Redux.

Habría que mover sus llamadas a la API al contenedor principal, Component A. Si desea que sus nietos se procesen solo después de que se completen las llamadas API, no puede mantener esas llamadas API en los nietos mismos. ¿Cómo se puede hacer una llamada API desde un componente que aún no existe?

Una vez que se realizan todas las llamadas a la API, puede usar acciones para actualizar una variable de estado global que contiene un montón de objetos de datos. Cada vez que se reciben datos (o se detecta un error), puede enviar una acción para verificar si su objeto de datos está completamente lleno. Una vez que se haya completado por completo, puede actualizar una loadingvariable falsey renderizar condicionalmente su Gridcomponente.

Así por ejemplo:

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

Su reductor contendrá el objeto de estado global que dirá si las cosas están listas para funcionar o no:

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

  }
}

En tus acciones:


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

Aparte

Si realmente está casado con la idea de que sus llamadas API viven dentro de cada nieto, pero que la cuadrícula completa de nietos no se procesa hasta que se completen todas las llamadas API, tendría que usar una solución completamente diferente. En este caso, sus nietos tendrían que ser representados desde el principio para hacer sus llamadas, pero tienen una clase css con display: none, que solo cambia después de que la variable de estado global loadingse marca como falsa. Esto también es factible, pero además del punto de Reaccionar.


1

Potencialmente podría resolver esto usando React Suspense.

La advertencia es que no es una buena idea suspender más allá del árbol de componentes que realiza el procesamiento (es decir: si su componente inicia un proceso de procesamiento, no es una buena idea suspender ese componente, en mi experiencia), por lo que es probablemente una mejor idea para iniciar las solicitudes en el componente que representa las celdas. Algo como esto:

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

Ahora, de qué manera el suspenso se combina con Redux, no estoy seguro. La idea general detrás de esta versión (¡experimental!) De Suspense es que inicie la búsqueda inmediatamente durante el ciclo de renderizado de un componente padre y pase un objeto que represente una búsqueda a los hijos. Esto evita que tenga que tener algún tipo de objeto de barrera (que necesitaría en otros enfoques).

¡Diré que no creo que esperar hasta que todo se haya recuperado para mostrar algo sea ​​el enfoque correcto porque la interfaz de usuario será tan lenta como la conexión más lenta o puede que no funcione en absoluto!

Aquí está el resto del código faltante:

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

Y un sandbox: https://codesandbox.io/s/falling-dust-b8e7s


1
solo lea los comentarios ... No creo que sea trivial esperar a que todos los componentes se procesen en el DOM, y eso es por una buena razón. Eso parece muy hacky de hecho. ¿Qué estás intentando lograr?
Dan Pantry

1

En primer lugar, el ciclo de vida podría tener un método asíncrono / espera.

Lo que necesitamos saber componentDidMounty componentWillMount,

componentWillMount se llama primero padre y luego hijo.
componentDidMount hace lo contrario.

En mi consideración, simplemente implementarlos usando el ciclo de vida normal sería lo suficientemente bueno.

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

Material-UI CircularProgress

Debido a que la representación infantil hace que los padres hagan lo mismo, atraparlo didUpdateestaría bien.
Y, dado que se llama después del renderizado, las páginas no deberían cambiarse después de eso si configura la función de verificación como su demanda.

Usamos esta implementación en nuestro producto que tiene una API que hace que los 30 obtengan la respuesta, todo funcionó bien hasta donde veo.

ingrese la descripción de la imagen aquí


0

Como usted está haciendo una llamada a la API en componentDidMount, cuando la llamada a la API resolverá tendrá los datos en componentDidUpdateque este ciclo de vida que va a pasar prevPropsy prevState. Entonces puedes hacer algo como a continuación:

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 desea saber esto antes de que se procese el componente, puede usarlo getSnapshotBeforeUpdate.

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


0

Hay varios enfoques con los que puede ir:

  1. La forma más fácil es pasar una devolución de llamada al componente hijo. Estos componentes secundarios pueden llamar a esta devolución de llamada tan pronto como se hayan procesado después de recuperar sus datos individuales o cualquier otra lógica de negocios que desee poner. Aquí hay un sandbox para lo mismo: https://codesandbox.io/s/unruffled-shockley-yf3f3
  2. Solicite a los componentes dependientes / secundarios que se actualicen cuando terminen de procesar los datos recuperados en un estado de aplicación global que su componente estaría escuchando
  3. También puede usar React.useContext para crear un estado localizado para este componente principal. Los componentes secundarios pueden actualizar este contexto cuando terminan de procesar los datos recuperados. Nuevamente, el componente padre estaría escuchando este contexto y puede actuar en consecuencia.
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.