React-親コンポーネントのすべてのサブコンポーネントがユーザーに表示されるときを検出する方法は?


11

TL; DR

親コンポーネントは、その下にあるすべての子コンポーネントのレンダリングが完了し、最新バージョンのDOMがユーザーに表示されるタイミングをどのようにして知ることができますか?

孫コンポーネントで構成されるcomponent AGridコンポーネントを持つがあるとし3x3ます。これらの孫コンポーネントはそれぞれ、Restful APIエンドポイントからデータをフェッチし、データが利用可能になるとそれ自体をレンダリングします。

私は、全体の面積をカバーしたいComponent Aloader、それはDOM上ですでにだと見ることができるように、グリッド内のコンポーネントの最後が正常にフェッチされたデータを持っているときにのみ発表し、それをレンダリングする、プレースホルダ。

ユーザーエクスペリエンスは、「ローダー」から、ちらつきのない完全に配置されたグリッドへの非常にスムーズな移行である必要があります。

私の問題は、ローダーの下のコンポーネントをいつ公開するかを正確に知ることです。

これを絶対的な精度で行うために信頼できるメカニズムはありますか?ローダーの時間制限をハードコーディングしないでください。私が理解ComponentDidMountしているように、呼び出し時にコンポーネントがユーザーに完全に表示されることを実際に保証するわけではないため、すべての子に依存することも信頼できません。

質問をさらに蒸留するには:

ある種のデータをレンダリングするコンポーネントがあります。それが初期化された後は持っていないので、その中componentDidMountでAPIエンドポイントにヒットします。データを受信すると、状態を変更して反映します。これは当然、そのコンポーネントの最終状態の再レンダリングを引き起こします。私の質問はこれです:それはしている再レンダリングするときに私が知っているどのように行わ、反射ユーザ直面DOMに。その時点!=コンポーネントの状態がデータを含むように変化した時点。


どのように状態を管理していますか?Reduxを使用したいですか?またはそれはコンポーネントを純粋に使い果たしていますか?
TechTurtle

コンポーネント内にありますが、外部化できます。私は他のもののためにReduxを使用します。
JasonGenX

グリッドの最後のコンポーネントがデータのフェッチを完了したことをどのようにして知るのでしょうか。
TechTurtle

私はしません。グリッドコンポーネントは互いに認識していません。データをフェッチするためComponentDidMountに使用axiosしているすべてのサブコンポーネントで。データが到着したら、そのコンポーネントの状態を変更して、データをレンダリングします。理論的には、8つの子コンポーネントは3秒以内にフェッチでき、最後のコンポーネントは15秒かかります...
JasonGenX

1
私はそれを行うことができますが、コンポーネントが「空」から「完全」にどのように変化するかをユーザーが決して見ないように、ローダーオーバーレイを発表するタイミングをどのように知ることができますか?これは、データの取得に関する問題ではありません。子コンポーネントが1つしかない場合でも、レンダリングについてです。ComponentDidMountでは不十分です。ローダーのオーバーレイを公開できるように、post-data-fetching-renderがいつ完了し、DOMが完全に更新されたかを知る必要があります。
JasonGenX

回答:


5

Reactには、コンポーネントのDOMがレンダリングされた後に呼び出される2つのライフサイクルフックがあります。

ユースケースでは、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のレンダリングメソッドでそれに応じて動作します。

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呼び出しが完了するまでレンダリングされない場合、完全に異なるソリューションを使用する必要があります。この場合、孫は呼び出しを行うために最初からレンダリングする必要がありますがdisplay: none、グローバル状態変数loadingがfalseとしてマークされた後にのみ変更されるを含むcssクラスが必要です。これも実行可能ですが、Reactのポイント以外にも、ある程度のことです。


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

さて、サスペンスがどのようにReduxとペアリングするのかはよくわかりません サスペンスのこの(実験的!)バージョンの背後にある全体的なアイデアは、親コンポーネントのレンダリングサイクル中にすぐにフェッチを開始し、フェッチを表すオブジェクトを子に渡すことです。これにより、ある種の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にレンダリングされるのを待つのは簡単なことではないと思います。それは正当な理由があります。それは確かに非常にハックのようです。何を達成しようとしていますか?
ダンパントリー

1

まず第一に、 ライフサイクルは非同期/待機メソッドを持つことができます。

知っておくべきこと 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 CircularProgress

子が再レンダリングすると親が同じことを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を使用して、この親コンポーネントのローカライズされた状態を作成することもできます。子コンポーネントは、フェッチされたデータのレンダリングが完了すると、このコンテキストを更新できます。この場合も、親コンポーネントはこのコンテキストをリッスンし、それに応じて動作できます
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.