Реагировать - Как определить, когда все подкомпоненты родительского компонента видны пользователю?

11

TL; DR

Как родительский компонент может знать, когда закончился рендеринг каждого дочернего компонента, и DOM виден пользователю с его самой последней версией?

Допустим, у меня component Aесть дочерний Gridкомпонент, состоящий из 3x3компонентов внука. Каждый из этих компонентов внука извлекает данные из конечной точки API успокоительного и отображает себя, когда данные становятся доступными.

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

Пользовательский опыт должен быть очень плавным переходом от «загрузчика» к полностью заполненной сетке без мерцания.

Моя проблема в том, чтобы точно знать, когда открыть компоненты под загрузчиком.

Есть ли механизм, на который я могу положиться, чтобы сделать это с абсолютной точностью? Я не жестко прописываю ограничение по времени для загрузчика. Как я понимаю, полагаться на ComponentDidMountкаждого ребенка также ненадежно, поскольку на самом деле он не гарантирует, что компонент полностью виден пользователю во время вызова.

Чтобы отогнать вопрос еще дальше:

У меня есть компонент, который отображает некоторые данные. После того, как он инициализирован, у него его нет, поэтому componentDidMountон обращается к конечной точке API для него. Как только он получает данные, он меняет свое состояние, чтобы отражать их. Понятно, что это вызывает повторную визуализацию конечного состояния этого компонента. Мой вопрос заключается в следующем: как я узнаю, когда этот повторный рендеринг произошел и отразился на DOM пользователя? Этот момент времени! = Момент времени, когда состояние компонента изменилось и теперь содержит данные.

JasonGenX
источник
Как управление государством? Как использовать Redux? или это чисто внутри компонентов?
TechTurtle
Это внутри компонентов, но может быть внешне. Я использую Redux для других вещей.
JasonGenX
и как вы узнали, что последний компонент в сетке завершил выборку данных?
TechTurtle
Я не. Компоненты сетки не знают друг друга. В каждом субкомпоненте ComponentDidMountя использую axiosдля извлечения данных. когда данные поступают, я меняю состояние того компонента, который вызывает рендеринг данных. Теоретически, 8 дочерних компонентов могут быть получены в течение 3 секунд, а последний займет 15 секунд ...
JasonGenX
1
Я могу это сделать, но как мне узнать, когда раскрыть оверлей загрузчика, чтобы пользователи никогда не увидели, как компонент переходит от «пустого» к «полному»? Это не вопрос о получении данных. Речь идет о рендеринге ... даже если бы у меня был только один дочерний компонент. ComponentDidMount недостаточно. Мне нужно знать, когда завершился post-data-fetching-render и 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 саспенса.

Предостережение заключается в том, что не следует делать приостановку за деревом компонентов, которое выполняет рендеринг (т. Е. Если ваш компонент запускает процесс рендеринга, то, по моему опыту, не рекомендуется приостанавливать этот компонент), поэтому Вероятно, лучшая идея начать запросы в компоненте, который отображает ячейки. Что-то вроде этого:

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 заключается в том, что вы запускаете выборку сразу во время цикла рендеринга родительского компонента и передаете объект, который представляет выборку, дочерним элементам. Это избавляет вас от необходимости иметь какой-либо объект Barrier (который вам понадобится в других подходах).

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

Вот остальная часть отсутствующего кода:

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,

componentWillMount называется сначала родительским, а затем дочерним.
componentDidMount делает обратное.

На мой взгляд, достаточно просто реализовать их с использованием нормального жизненного цикла.

  • дочерний компонент
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/>}

Материал-Пользовательский интерфейс CircularProgress

Так как дочерний повторный рендеринг заставляет родителя делать то же самое, поймать его didUpdateбыло бы хорошо.
И, так как он вызывается после рендеринга, страницы не должны изменяться после этого, если вы установили функцию проверки в качестве своего требования.

Мы используем эту реализацию в нашей программе, у которой есть API, вызывающий 30-х годов, чтобы получить ответ, насколько я вижу, все работало хорошо.

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

keikai
источник
0

Как вы делаете вызов апи в 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 для создания локализованного состояния для этого родительского компонента. Дочерние компоненты могут затем обновить этот контекст, когда они закончат с обработкой выбранных данных. Опять же, родительский компонент будет слушать этот контекст и может действовать соответственно
Рави Чаудхари
источник