React-如何检测父组件的所有子组件何时对用户可见?


11

TL; DR

父组件如何知道其下的每个子组件的渲染何时完成以及用户使用其最新版本的DOM可见?

假设我有一个component A具有Grid3x3孙组件组成的子组件的。这些孙子组件中的每个子组件都从宁静的API端点获取数据,并在数据可用时进行呈现。

我想Component A用一个loader占位符覆盖整个区域,仅当网格中的最后一个组件成功获取数据并呈现它,使得它已经在DOM上并且可以查看时才公开。

用户体验应该是从“加载程序”到完全填充的网格的超平滑过渡,而不会闪烁。

我的问题是确切知道何时在加载程序下发布组件。

我可以依靠任何机制绝对准确地做到这一点吗?我不对加载程序的时间进行硬编码。据我了解,ComponentDidMount对每个孩子的依赖也是不可靠的,因为它实际上并不能保证在通话时该组件对用户完全可见。

为了进一步提炼问题:

我有一个呈现某种数据的组件。初始化后就没有它了,因此它componentDidMount会命中一个API端点。一旦接收到数据,它就会更改其状态以反映它。可以理解的是,这将导致该组件的最终状态重新呈现。我的问题是:如何知道重新渲染何时发生反映在面向用户的DOM中。该时间点=组件状态已更改为包含数据的时间点。


如何管理国家?喜欢使用Redux?还是纯粹是组件?
TechTurtle

它在组件内部,但是可以外部化。我确实将Redux用于其他用途。
JasonGenX

您怎么知道网格中的最后一个组件已完成数据获取?
TechTurtle

我不。网格组件彼此之间不了解。在每个子组件ComponentDidMount我使用axios来获取数据。当数据通过时,我更改了导致数据渲染的组件的状态。从理论上讲,可以在3秒内提取8个子组件,而最后一个则需要15秒...
JasonGenX

1
我可以这样做,但是我怎么知道何时揭开加载程序覆盖图,以便用户永远看不到组件从“空”变为“满”的情况?这不是关于数据获取的问题。这是关于渲染...即使我只有一个子组件。ComponentDidMount还不够。我需要知道何时完成数据提取和渲染并且DOM已完全更新,以便我可以揭开加载程序覆盖图。
JasonGenX

Answers:


5

在组件的DOM呈现之后,React中有两个生命周期挂钩被调用:

对于您的用例,当N个子组件都满足某个条件X时,您的父组件P就会感兴趣。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的render方法中采取了相应的行动:

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标记为false 之后,该类才会更改。这也是可行的,但是除了React之外。


1

你可以潜在地解决这个使用作出反应悬念。

需要注意的是,将组件悬停在执行渲染的组件树上不是一个好主意(即:根据您的经验,如果您的组件启动了渲染过程,则不要将该组件挂起是一个好主意),因此大概一个更好的主意踢请求关闭,该组件使得细胞。像这样:

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对象(在其他方法中需要)。

我要说的是,我不认为等到一切都显示任何东西才是正确的方法,因为那样的话,UI会和最慢的连接一样慢,或者根本无法工作!

这是剩下的其余代码:

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中呈现会很简单-这是有充分理由的。确实,这似乎骇人听闻。您想达到什么目的?
Dan Pantry

1

首先,生命周期可能具有async / await方法。

我们需要了解什么componentDidMountcomponentWillMount

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

Material-UI 通告进度

由于孩子重新渲染导致父母也做同样的事情,所以抓住它didUpdate会很好。
并且,由于它是在渲染之后被调用的,因此如果您将check函数设置为需求,则此后不应更改页面。

我们在产品中使用此实现,该产品具有一个使30多岁的人获得响应的api,据我所知,它们都运行良好。

在此处输入图片说明


0

当您进行api调用componentDidMount时,API调用将解决时,您将获得数据,componentDidUpdate因为此生命周期将传递给您prevPropsprevState。因此,您可以执行以下操作:

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为该父组件创建本地化状态。然后,子组件可以在完成渲染获取的数据后更新此上下文。同样,父组件将监听此上下文并可以采取相应措施
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.