React - Як виявити, коли користувачі бачать усі підкомпоненти батьківського компонента?


11

TL; DR

Як батьківський компонент може знати, коли рендеринг кожного дочірнього компонента під ним закінчується і DOM стає видимим для користувача з його найновішою версією?

Скажімо, у мене є component Aдочірній Gridкомпонент, що складається з 3x3компонентів онука. Кожен з цих компонентів онука витягує дані з спокійної кінцевої точки API і видає себе, коли дані стають доступними.

Я хотів би, щоб покрити всю область Component Aз loaderзаповнювачем, буде відкрита тільки тоді , коли останні з компонентів в сітці мають сгружени дані успішно, і виніс його, таким чином, що це вже на DOM і може бути переглянуто.

Користувацький досвід повинен бути надто плавним переходом від "завантажувача" до повністю заселеної сітки без мерехтіння.

Моя проблема - це точно знати, коли розкрити компоненти під навантажувачем.

Чи є якийсь механізм, на який я можу покластися з абсолютною точністю? Я не жорстко кодую обмеження часу для навантажувача. Як я розумію, покладатися на ComponentDidMountкожну дитину також ненадійно, оскільки це фактично не гарантує, що компонент буде повністю видимий користувачеві під час дзвінка.

Щоб ще більше виготувати питання:

У мене є компонент, який видає якісь дані. Після його ініціалізації він не має його, тому в componentDidMountньому він потрапляє в кінцеву точку API для нього. Як тільки він отримує дані, він змінює свій стан, щоб відобразити їх. Це, зрозуміло, викликає повторне відображення остаточного стану цього компонента. Моє запитання таке: як я дізнаюсь, коли відбулося повторне відображення та відображається у користувачеві, що стикається з DOM. Цей момент часу! = Момент часу, коли стан компонента змінився, щоб містити дані.


Як керують державою? Любіть використовувати Redux? або це суто протиставлення компонентів?
TechTurtle

Це всередині компонентів, але може бути зовнішнім. Я використовую Redux для інших речей.
JasonGenX

і як ви знаєте, що останній компонент у сітці закінчив отримання даних?
TechTurtle

Я не. Компоненти сітки не знають один про одного. У всіх підкомпонентах, які ComponentDidMountя використовую axiosдля отримання даних. коли дані надходять, я змінюю стан того компонента, який викликає візуалізацію даних. Теоретично 8 дочірніх компонентів можуть отримати протягом 3 секунд, а на останній - 15 секунд ...
JasonGenX

1
Я можу це зробити, але як я знати, коли розкрити накладку завантажувача, щоб користувачі ніколи не побачили, як компонент переходить від "порожнього" до "повного"? Це не питання щодо отримання даних. Йдеться про надання… навіть якщо у мене був лише один дочірній компонент. ComponentDidMount недостатньо. Мені потрібно знати, коли закінчиться візуалізація після отримання даних, і DOM буде повністю оновлений, щоб я міг розкрити накладку завантажувача.
JasonGenX

Відповіді:


5

У React є два гачки життєвого циклу, які викликаються після надання DOM компонента:

Для вашого випадку використання ваш батьківський компонент P зацікавлений , коли N дочірні компоненти мають кожен задовольняється деякий умова X . X можна визначити як послідовність:

  • операція асинхронізації завершена
  • компонент надав

Поєднуючи стан компонента і використовуючи componentDidUpdateгачок, ви можете дізнатися, коли послідовність завершена і ваш компонент відповідає умові X.

Ви можете відслідковувати, коли ваша операція асинхронізації завершена, встановивши змінну стану. Наприклад:

this.setState({isFetched: true})

Після встановлення стану React викликає componentDidUpdateфункцію ваших компонентів . Порівнюючи поточні та попередні об'єкти стану в межах цієї функції, ви можете сигналізувати батьківському компоненту про те, що ваша операція асинхронізації завершена і стан вашого нового компонента надано:

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

У своєму компоненті P ви можете використовувати лічильник, щоб відстежувати, скільки дітей значимо оновили:

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

Нарешті, знаючи довжину N, ви можете знати, коли відбулися всі значущі оновлення та діяти відповідно у вашому методі візуалізації P :

const childRenderingFinished = this.state.counter >= N

1

Я налаштував би це так, щоб ви покладалися на змінну глобального стану, щоб сказати своїм компонентам, коли потрібно відображати. Redux краще для цього сценарію, коли багато компонентів розмовляють один з одним, і ви згадали в коментарі, що ви інколи використовуєте його. Тож я намалюю відповідь за допомогою Redux.

Ви повинні були б перемістити API викликів в батьківський контейнер, Component A. Якщо ви хочете, щоб ваші онуки здійснювали візуалізацію лише після завершення викликів API, ви не можете зберігати ці дзвінки API у власних онуків. Як можна зробити дзвінок API з компонента, який ще не існує?

Після того, як всі виклики API здійснені, ви можете використовувати дії для оновлення змінної глобального стану, що містить купу об'єктів даних. Щоразу, коли дані отримуються (або виявляється помилка), ви можете відправити дію, щоб перевірити, чи повністю заповнений ваш об'єкт даних. Після її повного заповнення ви можете оновити loadingзмінну до falseта умовно візуалізувати ваш Gridкомпонент.

Так, наприклад:

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

Ваш редуктор утримує об’єкт глобального стану, який скаже, чи все вже готове до роботи чи ні:

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

  }
}

У ваших діях:


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

Убік

Якщо ви дійсно одружені з ідеєю, що ваші дзвінки API живуть у кожного онука, але вся сітка онуків не надає, поки всі виклики API не будуть завершені, вам доведеться використовувати зовсім інше рішення. У цьому випадку вашим онукам потрібно було б відмовитись з самого початку, щоб здійснити їх дзвінки, але мати клас css display: none, який змінюється лише після того, як глобальна змінна стан loadingбуде позначена як помилкова. Це теж можливо, але начебто, крім точки Реагування.


1

Ви можете потенційно вирішити це за допомогою React Suspense.

Застереження полягає в тому, що не годиться призупиняти повз дерево компонентів, яке робить візуалізацію (тобто: якщо ваш компонент починає процес візуалізації, на мій досвід, це не гарна ідея призупиняти цей компонент), тож це напевно, краща ідея відкинути запити в компоненті, що надає комірки. Щось на зразок цього:

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

Зараз я точно не знаю, як Suspense поєднується з Redux. Вся ідея, що стоїть за цією (експериментальною!) Версією Suspense, полягає в тому, що ви запускаєте завантаження негайно під час циклу візуалізації батьківського компонента і передаєте дітям об'єкт, що представляє собою збір. Це заважає вам мати якийсь об'єкт бар'єру (який вам знадобиться в інших підходах).

Я скажу, що я не думаю, що чекати, поки все з’явиться, щоб щось відобразити, - це правильний підхід, тому що тоді інтерфейс користувача буде настільки ж повільним, як і найповільніший зв’язок або може не працювати взагалі!

Ось решта відсутнього коду:

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

І пісочниця: https://codesandbox.io/s/falling-dust-b8e7s


1
просто читайте коментарі. Я не вірю, що було б тривіально чекати, коли всі компоненти будуть відображені в DOM - і це з поважної причини. Це здається дуже хакі. Чого ви намагаєтесь досягти?
Ден Комора

1

Перш за все, життєвий цикл може мати метод асинхронізації / очікування.

Що нам потрібно знати про componentDidMountі componentWillMount,

компонентWillMount називається першим батьком, а потім дочірнім.
компонентDidMount зробити навпаки.

На мій погляд, просто реалізувати їх за допомогою нормального життєвого циклу було б досить добре.

  • дочірній компонент
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();
}
  • батьківський компонент
// Initial state `loading: true`
componentDidUpdate() {
  this.checkIfResHaveBad(); // If all good, `loading: false`
}
...
{this.state.loading ? <CircularProgress /> : <YourComponent/>}

Матеріал- круговий прогрес у користувальницькому інтерфейсі

Оскільки повторне надання дитини змушує батьків зробити те саме, спіймати це didUpdateбуло б добре.
Оскільки вони називаються після надання, сторінки не слід змінювати після цього, якщо ви встановите функцію перевірки як свою вимогу.

Ми використовуємо цю реалізацію в нашому продукті, у якого api викликає реакцію 30-х років, і все добре працювало, наскільки я бачу.

введіть тут опис зображення


0

Коли ви здійснюєте api-дзвінок componentDidMount, коли виклик API вирішиться, ви отримаєте дані, componentDidUpdateоскільки цей життєвий цикл пройде вас prevPropsі prevState. Тож ви можете зробити щось на кшталт нижче:

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

Якщо ви хочете знати це до того, як компонент відобразиться, ви можете використовувати getSnapshotBeforeUpdate.

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


0

Існує кілька підходів, з якими можна скористатися:

  1. Найпростіший спосіб - передати зворотний дзвінок до дочірнього компонента. Потім ці дочірні компоненти можуть викликати цей зворотний дзвінок, як тільки вони відображаються після отримання їхніх індивідуальних даних або будь-якої іншої логіки бізнесу, яку ви хочете ввести. Ось пісочниця для того ж: https://codesandbox.io/s/unruffled-shockley-yf3f3
  2. Попросіть залежні / дочірні компоненти оновити, коли вони будуть виконані з наданням отриманих даних до глобального стану програми, який ваш компонент слухав би
  3. Ви також можете використовувати React.useContext для створення локалізованого стану для цього батьківського компонента. Потім дочірні компоненти можуть оновити цей контекст, коли вони завершаться із наданням отриманих даних. Знову ж таки, батьківський компонент слухатиме цей контекст і може діяти відповідно
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.