React - Como detectar quando todos os subcomponentes de um componente pai estão visíveis para o usuário?

11

TL; DR

Como um componente pai pode saber quando a renderização de cada componente filho abaixo dele foi concluída e o DOM visível ao usuário com sua versão mais atualizada?

Digamos que eu possua um componente component Afilho Gridcomposto por 3x3componentes netos. Cada um desses componentes netos busca dados de um ponto de extremidade da API repousante e se processa quando os dados ficam disponíveis.

Eu gostaria de cobrir toda a área de Component Acom um loaderespaço reservado, a ser revelado somente quando o último dos componentes da grade buscar os dados com êxito e renderizá-los, de modo que eles já estejam no DOM e possam ser visualizados.

A experiência do usuário deve ser uma transição super suave do "carregador" para uma grade totalmente preenchida sem tremer.

Meu problema é saber exatamente quando desvendar os componentes sob o carregador.

Existe algum mecanismo em que eu possa confiar para fazer isso com precisão absoluta? Não codifico um limite de tempo para o carregador. Pelo que entendi, confiar em ComponentDidMounttodas as crianças também não é confiável, pois na verdade não garante que o componente esteja totalmente visível para o usuário no momento da chamada.

Para destilar ainda mais a questão:

Eu tenho um componente que renderiza algum tipo de dados. Depois de inicializado, ele não o possui; portanto componentDidMount, atinge um ponto de extremidade da API. Depois de receber os dados, ele muda de estado para refleti-los. Isso, compreensivelmente, causa uma nova renderização do estado final desse componente. Minha pergunta é a seguinte: como sei quando essa nova renderização ocorreu e se reflete no usuário que está enfrentando o DOM. Esse ponto no tempo! = O ponto no tempo em que o estado do componente mudou para conter dados.

JasonGenX
fonte
Como estão gerenciando o estado? Gosta de usar o Redux? ou é puramente dentro dos componentes?
TechTurtle 14/02
Está dentro dos componentes, mas pode ser externalizado. Eu uso o Redux para outras coisas.
JasonGenX 14/02
e como você sabe que o último componente da grade terminou de buscar dados?
TechTurtle 14/02
Eu não. Os componentes da grade não estão cientes um do outro. Em todos os subcomponentes ComponentDidMountque estou usando axiospara buscar dados. quando os dados chegam, altero o estado desse componente que causa uma renderização dos dados. Em teoria, 8 componentes filhos podem ser buscados em 3 segundos e o último levaria 15 segundos ...
JasonGenX em 14/02
11
Eu posso fazer isso, mas como sei quando revelar a sobreposição do carregador de forma que os usuários nunca vejam como um componente passa de "vazio" para "cheio"? Esta não é uma pergunta sobre a busca de dados. É sobre renderização ... mesmo se eu tivesse apenas um componente filho. ComponentDidMount não é suficiente. Preciso saber quando a renderização pós-busca de dados foi concluída e o DOM está totalmente atualizado para que eu possa revelar a sobreposição do carregador.
JasonGenX 14/02

Respostas:

5

Existem dois ganchos de ciclo de vida no React que são chamados após a renderização do DOM de um componente:

Para o seu caso de uso seu pai componente P está interessado quando N componentes filhos têm cada satisfeito alguma condição X . X pode ser definido como uma sequência:

  • operação assíncrona concluída
  • componente processado

Ao combinar o estado do componente e usar o componentDidUpdategancho, você pode saber quando a sequência foi concluída e seu componente atende à condição X.

Você pode acompanhar quando sua operação assíncrona foi concluída definindo uma variável de estado. Por exemplo:

this.setState({isFetched: true})

Após definir o estado, o React chamará a componentDidUpdatefunção de componentes . Ao comparar os objetos de estado atual e anterior nessa função, você pode sinalizar para o componente pai que sua operação assíncrona foi concluída e que o estado do novo componente foi renderizado:

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

No seu componente P, você pode usar um contador para acompanhar quantas crianças foram atualizadas significativamente:

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

Por fim, sabendo o tamanho de N, você pode saber quando todas as atualizações significativas ocorreram e agir de acordo com o método de renderização de P :

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

Eu o configuraria para que você confie em uma variável de estado global para informar seus componentes quando renderizar. O Redux é melhor para esse cenário em que muitos componentes estão conversando entre si e você mencionou em um comentário que o usa algumas vezes. Então, esboçarei uma resposta usando o Redux.

Você precisaria mover suas chamadas de API para o contêiner pai Component A. Se você deseja que seus netos sejam renderizados somente após a conclusão das chamadas da API, não será possível mantê-las nos próprios netos. Como uma chamada de API pode ser feita a partir de um componente que ainda não existe?

Depois que todas as chamadas de API são feitas, você pode usar ações para atualizar uma variável de estado global que contém vários objetos de dados. Sempre que os dados são recebidos (ou um erro é detectado), você pode despachar uma ação para verificar se o seu objeto de dados está totalmente preenchido. Uma vez preenchido completamente, você pode atualizar uma loadingvariável falsee renderizar condicionalmente seu Gridcomponente.

Então, por exemplo:

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

Seu redutor conterá o objeto de estado global, que dirá se tudo já está pronto para ser executado:

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

  }
}

Em suas ações:


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, de lado

Se você é realmente casado com a ideia de que suas chamadas à API residem em cada neto, mas que toda a grade de netos não é processada até que todas as chamadas sejam concluídas, você precisará usar uma solução completamente diferente. Nesse caso, seus netos precisariam ser renderizados desde o início para fazer suas chamadas, mas terão uma classe css com display: none, que somente será alterada após a variável de estado global loadingser marcada como falsa. Isso também é possível, mas além do ponto de React.

Seth Lutske
fonte
1

Você pode potencialmente resolver isso usando o React Suspense.

A ressalva é que não é uma boa ideia Suspender a árvore de componentes que faz a renderização (ou seja: se o seu componente iniciar um processo de renderização, não é uma boa ideia suspender esse componente, na minha experiência), por isso é provavelmente uma idéia melhor para iniciar as solicitações no componente que renderiza as células. Algo assim:

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

Agora, como Suspense combina com Redux, não tenho certeza. A idéia por trás dessa versão (experimental!) Do Suspense é que você inicie a busca imediatamente durante o ciclo de renderização de um componente pai e passe um objeto que represente uma busca para os filhos. Isso impede que você precise ter algum tipo de objeto Barreira (o que você precisaria em outras abordagens).

Vou dizer que não acho que esperar até que tudo tenha buscado para exibir alguma coisa seja a abordagem correta, porque a interface do usuário será tão lenta quanto a conexão mais lenta ou poderá não funcionar!

Aqui está o restante do código ausente:

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 uma caixa de areia: https://codesandbox.io/s/falling-dust-b8e7s

Dan Pantry
fonte
11
basta ler os comentários. Não acredito que seria trivial esperar que todos os componentes sejam renderizados no DOM - e isso é por um bom motivo. Isso parece muito hacky de fato. O que você está tentando alcançar?
Dan Pantry
1

Primeiro de tudo, o ciclo de vida pode ter um método assíncrono / aguardado.

O que precisamos saber componentDidMounte componentWillMount,

componentWillMount é chamado primeiro pai e filho.
componentDidMount faça o oposto.

Na minha consideração, simplesmente implementá-los usando o ciclo de vida normal seria bom o suficiente.

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

Interface do usuário CircularProgress

Como a nova renderização do filho, fazendo com que o pai faça o mesmo, capturá-lo didUpdateseria bom.
E, como é chamado após a renderização, as páginas não devem ser alteradas depois disso, se você definir a função de verificação como sua demanda.

Usamos essa implementação em nosso produto, que possui uma API que leva 30s a obter a resposta, tudo funcionou bem até onde eu vejo.

insira a descrição da imagem aqui

keikai
fonte
0

Quando você faz uma chamada de API componentDidMount, quando a chamada da API for resolvida, você terá os dados, componentDidUpdatepois esse ciclo de vida passará por você prevPropse prevState. Então você pode fazer algo como abaixo:

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 você deseja saber isso antes que o componente seja renderizado novamente, você pode usá-lo getSnapshotBeforeUpdate.

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

Pranay Tripathi
fonte
0

Existem várias abordagens que você pode seguir:

  1. A maneira mais fácil é passar um retorno de chamada para o componente filho. Esses componentes filhos podem chamar esse retorno de chamada assim que forem renderizados após a busca de dados individuais ou qualquer outra lógica comercial que você queira colocar. Aqui está uma caixa de areia para o mesmo: https://codesandbox.io/s/unruffled-shockley-yf3f3
  2. Peça aos componentes dependentes / filhos para atualizarem quando terminarem de renderizar dados buscados para um estado global do aplicativo que seu componente estaria ouvindo
  3. Você também pode usar React.useContext para criar um estado localizado para esse componente pai. Os componentes filhos podem atualizar esse contexto quando terminar a renderização de dados buscados. Novamente, o componente pai estaria ouvindo esse contexto e pode agir de acordo
Ravi Chaudhary
fonte