React - Jak wykryć, kiedy wszystkie podskładniki komponentu macierzystego są widoczne dla użytkownika?

11

TL; DR

Skąd komponent nadrzędny może wiedzieć, kiedy renderowanie każdego komponentu podrzędnego pod nim zakończyło się, a DOM jest widoczny dla użytkownika w jego najnowszej wersji?

Powiedzmy, że mam komponent component Apotomny Gridskładający się z 3x3komponentów wnuka. Każdy z tych składników wnuka pobiera dane z punktu końcowego interfejsu API, który się uspokaja i renderuje, gdy dane stają się dostępne.

Chciałbym aby pokryć całą powierzchnię Component Az loaderzastępczy, który zostanie zaprezentowany dopiero, gdy ostatni z elementów w siatce ma idącą dane skutecznie i renderowane go tak, że to już na DOM i mogą być przeglądane.

Interfejs użytkownika powinien być bardzo płynnym przejściem z „modułu ładującego” do całkowicie wypełnionej siatki bez migotania.

Mój problem polega na tym, by dokładnie wiedzieć, kiedy odsłonić komponenty pod modułem ładującym.

Czy jest jakiś mechanizm, na którym mogę polegać, aby robić to z absolutną dokładnością? Nie koduję na stałe limitu czasu dla modułu ładującego. Rozumiem, że poleganie na ComponentDidMountkażdym dziecku jest również zawodne, ponieważ w rzeczywistości nie gwarantuje, że element będzie w pełni widoczny dla użytkownika w momencie połączenia.

Aby jeszcze bardziej wyjaśnić pytanie:

Mam komponent, który renderuje jakiś rodzaj danych. Po zainicjowaniu nie ma go, więc componentDidMounttrafia do niego punkt końcowy interfejsu API. Po otrzymaniu danych zmienia stan, aby je odzwierciedlić. To zrozumiałe powoduje ponowne renderowanie stanu końcowego tego komponentu. Moje pytanie brzmi: skąd mam wiedzieć, kiedy to ponowne renderowanie miało miejsce i jest odzwierciedlone w DOM zwróconym do użytkownika? Ten moment w czasie! = Moment, w którym stan komponentu zmienił się i zawiera dane.

JasonGenX
źródło
Jak zarządza państwem? Lubisz korzystać z Redux? czy to wyłącznie z komponentami?
TechTurtle
Jest w komponentach, ale można go uzewnętrznić. Używam Redux do innych rzeczy.
JasonGenX
i skąd wiesz, że ostatni składnik w siatce zakończył pobieranie danych?
TechTurtle
Ja nie. Komponenty siatki nie są sobie świadome. W każdym podskładniku ComponentDidMountużywam axiosdo pobierania danych. kiedy dane przechodzą, zmieniam stan tego komponentu, który powoduje renderowanie danych. Teoretycznie 8 komponentów potomnych może zostać pobranych w ciągu 3 sekund, a ostatni zajmie 15 sekund ...
JasonGenX
1
Mogę to zrobić, ale skąd mam wiedzieć, kiedy odsłonić nakładkę modułu ładującego, aby użytkownicy nigdy nie widzieli, jak składnik przechodzi z „pustego” do „pełnego”? To nie jest pytanie o pobieranie danych. Chodzi o renderowanie ... nawet gdybym miał tylko jeden element potomny. ComponentDidMount to za mało. Muszę wiedzieć, kiedy renderowanie po pobraniu danych zakończyło się, a DOM jest w pełni zaktualizowany, aby móc odsłonić nakładkę modułu ładującego.
JasonGenX

Odpowiedzi:

5

Istnieją dwa zaczepy cyklu życia w React, które są wywoływane po renderowaniu DOM komponentu:

Dla Państwa przypadku użycia rodzic składnik P jest zainteresowany, gdy N komponenty dziecko ma każdy spełniony pewien warunek X . X można zdefiniować jako sekwencję:

  • operacja asynchroniczna zakończona
  • składnik został zrenderowany

Łącząc stan komponentu i używając componentDidUpdatehaka, możesz wiedzieć, kiedy sekwencja została zakończona, a komponent spełnia warunek X.

Możesz śledzić zakończenie operacji asynchronicznej, ustawiając zmienną stanu. Na przykład:

this.setState({isFetched: true})

Po ustawieniu stanu React wywoła componentDidUpdatefunkcję składników . Porównując bieżące i poprzednie obiekty stanu w ramach tej funkcji, możesz zasygnalizować komponentowi nadrzędnemu, że operacja asynchroniczna została zakończona, a stan nowego komponentu jest renderowany:

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

W komponencie P możesz użyć licznika, aby śledzić, ile dzieci znacząco zaktualizowało:

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

Wreszcie, znając długość N , możesz wiedzieć, kiedy nastąpiły wszystkie znaczące aktualizacje, i działaj odpowiednio w metodzie renderowania P :

const childRenderingFinished = this.state.counter >= N
Andrew Sinner
źródło
1

Ustawiłbym to tak, abyś polegał na globalnej zmiennej stanu, aby powiedzieć swoim komponentom, kiedy ma być renderowany. Redux jest lepszy w tym scenariuszu, w którym wiele komponentów ze sobą rozmawia, a wspomniałeś w komentarzu, że czasami go używasz. Więc naszkicuję odpowiedź za pomocą Redux.

Trzeba przenieść wywołań API do kontenera macierzystego Component A. Jeśli chcesz, aby wnuki były renderowane tylko po zakończeniu wywołań API, nie możesz zachować tych wywołań API w samych wnukach. Jak można wywołać API z komponentu, który jeszcze nie istnieje?

Po wykonaniu wszystkich wywołań interfejsu API można użyć akcji do zaktualizowania globalnej zmiennej stanu zawierającej kilka obiektów danych. Za każdym razem, gdy dane są odbierane (lub wychwytywany jest błąd), możesz wysłać akcję, aby sprawdzić, czy obiekt danych jest całkowicie wypełniony. Po jej całkowitym wypełnieniu możesz zaktualizować loadingzmienną falsei warunkowo renderować Gridkomponent.

Na przykład:

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

Twój reduktor zatrzyma globalny obiekt stanu, który powie, czy wszystko jest już gotowe, czy nie:

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

  }
}

W twoich działaniach:


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

Na bok

Jeśli jesteś naprawdę żonaty z myślą, że Twoje wywołania API znajdują się wewnątrz każdego wnuka, ale że cała siatka wnuków nie jest renderowana, dopóki wszystkie wywołania API nie zostaną zakończone, musisz użyć zupełnie innego rozwiązania. W takim przypadku wasze wnuki musiałyby być renderowane od samego początku, aby wykonywać połączenia, ale miały klasę css display: none, która zmienia się dopiero po tym, jak globalna zmienna stanu loadingjest oznaczona jako fałsz. Jest to również wykonalne, ale w pewnym sensie poza React.

Seth Lutske
źródło
1

Można potencjalnie rozwiązać ten problem za pomocą React napięciu.

Zastrzeżenie polega na tym, że nie jest dobrym pomysłem zawieszenie się obok drzewa komponentów, które wykonuje renderowanie (to znaczy: jeśli twój komponent rozpoczyna proces renderowania, moim zdaniem zawieszenie tego komponentu nie jest dobrym pomysłem), więc prawdopodobnie lepszym pomysłem jest wykasowanie żądań w komponencie renderującym komórki. Coś takiego:

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

Nie jestem pewien, jak bardzo Suspens łączy się z Redux. Cała idea tej (eksperymentalnej!) Wersji Suspense polega na tym, że pobieranie rozpoczyna się natychmiast podczas cyklu renderowania komponentu nadrzędnego i przekazuje obiekt, który reprezentuje pobieranie do dzieci. Zapobiega to konieczności posiadania pewnego rodzaju obiektu Barrier (który byłby potrzebny w innych podejściach).

Powiem, że nie sądzę, że czekanie, aż wszystko się pobierze, aby wyświetlić cokolwiek, jest właściwym podejściem, ponieważ wtedy interfejs użytkownika będzie tak wolny jak najwolniejsze połączenie lub może w ogóle nie działać!

Oto pozostała część brakującego kodu:

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

I piaskownica: https://codesandbox.io/s/falling-dust-b8e7s

Dan Pantry
źródło
1
po prostu przeczytaj komentarze. Nie sądzę, aby czekanie na renderowanie wszystkich komponentów w DOM było banalne - i to nie bez powodu. To naprawdę wydaje się bardzo zaraźliwe. Co próbujesz osiągnąć?
Dan Pantry
1

Przede wszystkim cykl życia może mieć metodę asynchroniczną / oczekującą.

Co musimy wiedzieć o componentDidMount, a componentWillMount,

componentWillMount jest nazywany najpierw rodzicem, a następnie dzieckiem.
componentDidMount robi odwrotnie.

Moim zdaniem wystarczy je wdrożyć w normalnym cyklu życia.

  • składnik potomny
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();
}
  • składnik nadrzędny
// Initial state `loading: true`
componentDidUpdate() {
  this.checkIfResHaveBad(); // If all good, `loading: false`
}
...
{this.state.loading ? <CircularProgress /> : <YourComponent/>}

Material-UI CircularProgress

Ponieważ ponowne renderowanie dziecka powoduje, że rodzic robi to samo, złapanie go didUpdatebyłoby w porządku.
A ponieważ jest wywoływany po renderowaniu, strony nie powinny być później zmieniane, jeśli ustawisz funkcję sprawdzania jako swoje żądanie.

Używamy tej implementacji w naszym produkcie, który ma interfejs API, co powoduje, że 30s otrzymuje odpowiedź, wszystko działało dobrze, o ile widzę.

wprowadź opis zdjęcia tutaj

keikai
źródło
0

Gdy wykonujesz wywołanie interfejsu API componentDidMount, gdy połączenie API zostanie rozwiązane, będziesz mieć dane, componentDidUpdateponieważ ten cykl życia minie prevPropsi prevState. Możesz więc zrobić coś takiego:

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

Jeśli chcesz to wiedzieć przed ponownym renderowaniem komponentu, możesz użyć getSnapshotBeforeUpdate.

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

Pranay Tripathi
źródło
0

Istnieje wiele metod, które możesz zastosować:

  1. Najprostszym sposobem jest przekazanie wywołania zwrotnego do elementu potomnego. Te komponenty potomne mogą następnie wywołać to wywołanie zwrotne natychmiast po ich renderowaniu po pobraniu ich indywidualnych danych lub innej logiki biznesowej, którą chcesz wprowadzić. Oto piaskownica dla tego samego: https://codesandbox.io/s/unruffled-shockley-yf3f3
  2. Poproś składniki zależne / potomne o aktualizację, gdy zakończą się renderowaniem pobranych danych do globalnego stanu aplikacji, którego słuchałby twój komponent
  3. Możesz także użyć React.useContext, aby utworzyć zlokalizowany stan dla tego komponentu nadrzędnego. Komponenty podrzędne mogą następnie zaktualizować ten kontekst, gdy zakończą się renderowaniem pobranych danych. Ponownie, komponent nadrzędny będzie nasłuchiwał tego kontekstu i może działać odpowiednio
Ravi Chaudhary
źródło