Phản ứng - Làm thế nào để phát hiện khi tất cả các thành phần phụ của thành phần cha mẹ hiển thị cho người dùng?


11

TL; DR

Làm thế nào một thành phần cha mẹ có thể biết khi kết xuất của mọi thành phần con bên dưới nó đã kết thúc và DOM hiển thị cho người dùng với phiên bản cập nhật nhất của nó?

Giả sử tôi có một thành phần component Acon Gridbao gồm các 3x3thành phần cháu. Mỗi thành phần cháu đó lấy dữ liệu từ điểm cuối API yên tĩnh và hiển thị chính nó khi dữ liệu có sẵn.

Tôi muốn bao phủ toàn bộ khu vực Component Avới một loadertrình giữ chỗ, chỉ được tiết lộ khi phần cuối cùng của lưới đã lấy dữ liệu thành công và hiển thị nó, sao cho nó đã có trên DOM và có thể xem được.

Trải nghiệm người dùng phải là một quá trình chuyển đổi siêu mượt mà từ "trình tải" sang lưới được điền đầy đủ mà không nhấp nháy.

Vấn đề của tôi là biết chính xác khi nào để tiết lộ các thành phần dưới bộ nạp.

Có cơ chế nào tôi có thể dựa vào để làm điều này với độ chính xác tuyệt đối không? Tôi không mã hóa giới hạn thời gian cho trình tải. Theo tôi hiểu, việc dựa vào ComponentDidMountmọi đứa trẻ cũng không đáng tin cậy vì nó không thực sự đảm bảo thành phần này được hiển thị đầy đủ cho người dùng tại thời điểm cuộc gọi.

Để chắt lọc câu hỏi hơn nữa:

Tôi có một thành phần làm cho một số loại dữ liệu. Sau khi được khởi tạo, nó không có nó, vì vậy, componentDidMountnó đạt điểm cuối API cho nó. Khi nó nhận được dữ liệu, nó sẽ thay đổi trạng thái để phản ánh nó. Điều này dễ hiểu gây ra kết xuất lại trạng thái cuối cùng của thành phần đó. Câu hỏi của tôi là: Làm thế nào để tôi biết khi kết xuất lại đó đã diễn ra và được phản ánh trong DOM đối diện với Người dùng. Thời điểm đó! = Thời điểm khi trạng thái của thành phần đã thay đổi để chứa dữ liệu.


Quản lý nhà nước như thế nào? Thích sử dụng Redux? hoặc nó hoàn toàn là héo các thành phần?
TechTurtle

Đó là trong các thành phần, nhưng có thể được bên ngoài. Tôi sử dụng Redux cho những thứ khác.
JasonGenX

và làm thế nào để bạn biết rằng thành phần cuối cùng trong lưới đã tải xong dữ liệu?
TechTurtle

Tôi không. Các thành phần lưới không nhận thức được nhau. Trong mọi thành phần phụ ComponentDidMounttôi đang sử dụng axiosđể tìm nạp dữ liệu. khi dữ liệu đi qua, tôi thay đổi trạng thái của thành phần đó gây ra kết xuất dữ liệu. Về lý thuyết, 8 thành phần con có thể tìm nạp trong vòng 3 giây và thành phần cuối cùng sẽ mất 15 giây ...
JasonGenX

1
Tôi có thể làm điều đó, nhưng làm cách nào để biết khi nào tiết lộ lớp phủ trình tải sao cho người dùng sẽ không bao giờ thấy một thành phần chuyển từ "trống" sang "đầy đủ" như thế nào? Đây không phải là một câu hỏi về tìm nạp dữ liệu. Đó là về kết xuất ... ngay cả khi tôi chỉ có một thành phần con. ElementDidMount không đủ. Tôi cần biết khi hoàn thành quá trình tải dữ liệu sau khi hoàn thành và DOM được cập nhật đầy đủ để tôi có thể tiết lộ lớp phủ của trình tải.
JasonGenX

Câu trả lời:


5

Có hai móc vòng đời trong React được gọi sau khi DOM của thành phần được hiển thị:

Đối với trường hợp sử dụng của bạn, thành phần cha mẹ P của bạn quan tâm khi N thành phần con có mỗi điều kiện X thỏa mãn . X có thể được định nghĩa là một chuỗi:

  • hoàn thành thao tác async
  • thành phần đã kết xuất

Bằng cách kết hợp trạng thái của thành phần và sử dụng componentDidUpdatehook, bạn có thể biết khi nào chuỗi đã hoàn thành và thành phần của bạn đáp ứng điều kiện X.

Bạn có thể theo dõi thời điểm hoạt động không đồng bộ của bạn đã hoàn thành bằng cách đặt biến trạng thái. Ví dụ:

this.setState({isFetched: true})

Sau khi thiết lập trạng thái, React sẽ gọi componentDidUpdatechức năng thành phần của bạn . Bằng cách so sánh các đối tượng trạng thái hiện tại và trước đó trong chức năng này, bạn có thể báo hiệu cho thành phần cha mẹ rằng hoạt động không đồng bộ của bạn đã hoàn thành và trạng thái của thành phần mới của bạn đã được hiển thị:

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

Trong thành phần P của bạn, bạn có thể sử dụng bộ đếm để theo dõi số lượng trẻ em đã cập nhật một cách có ý nghĩa:

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

Cuối cùng, bằng cách biết độ dài của N, bạn có thể biết khi nào tất cả các cập nhật có ý nghĩa đã xảy ra và hành động tương ứng trong phương thức kết xuất P của bạn :

const childRenderingFinished = this.state.counter >= N

1

Tôi sẽ thiết lập nó để bạn dựa vào một biến trạng thái toàn cầu để cho biết các thành phần của bạn khi nào sẽ kết xuất. Redux tốt hơn cho kịch bản này khi nhiều thành phần đang nói chuyện với nhau và đôi khi bạn đã đề cập trong một nhận xét rằng đôi khi bạn sử dụng nó. Vì vậy, tôi sẽ phác thảo một câu trả lời bằng Redux.

Bạn sẽ phải chuyển các lệnh gọi API của mình sang vùng chứa cha , Component A. Nếu bạn muốn cho cháu của mình kết xuất chỉ sau khi các lệnh gọi API hoàn tất, bạn không thể giữ các cuộc gọi API đó cho chính các cháu của mình. Làm thế nào một cuộc gọi API có thể được thực hiện từ một thành phần chưa tồn tại?

Khi tất cả các lệnh gọi API được thực hiện, bạn có thể sử dụng các hành động để cập nhật biến trạng thái toàn cục có chứa một loạt các đối tượng dữ liệu. Mỗi khi dữ liệu được nhận (hoặc một lỗi được bắt gặp), bạn có thể gửi một hành động để kiểm tra xem đối tượng dữ liệu của bạn đã được điền đầy đủ chưa. Khi nó được điền đầy đủ, bạn có thể cập nhật một loadingbiến falsevà hiển thị có điều kiện Gridthành phần của bạn .

Ví dụ:

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

Trình giảm tốc của bạn sẽ giữ đối tượng trạng thái toàn cầu sẽ cho biết mọi thứ đã sẵn sàng hay chưa:

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

  }
}

Trong hành động của bạn:


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

Qua một bên

Nếu bạn thực sự kết hôn với ý tưởng rằng các cuộc gọi API của bạn sống bên trong mỗi đứa cháu, nhưng toàn bộ Lưới cháu không kết xuất cho đến khi tất cả các lệnh gọi API được hoàn thành, bạn phải sử dụng một giải pháp hoàn toàn khác. Trong trường hợp này, cháu của bạn sẽ phải được kết xuất ngay từ đầu để thực hiện cuộc gọi của chúng, nhưng có một lớp css với display: none, chỉ thay đổi sau khi biến trạng thái toàn cầu loadingđược đánh dấu là sai. Điều này cũng có thể thực hiện được, nhưng bên cạnh quan điểm của React.


1

Bạn có khả năng có thể giải quyết điều này bằng cách sử dụng React Suspense.

Thông báo trước là không nên tạm dừng qua cây thành phần thực hiện kết xuất (đó là: Nếu thành phần của bạn khởi động quá trình kết xuất, theo kinh nghiệm của tôi, không nên tạm dừng thành phần đó) có lẽ là một ý tưởng tốt hơn để loại bỏ các yêu cầu trong thành phần kết xuất các ô. Một cái gì đó như thế này:

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

Bây giờ, khá chắc chắn làm thế nào các cặp Hồi hộp với Redux tôi không chắc chắn. Toàn bộ ý tưởng đằng sau phiên bản Suspense (thử nghiệm!) Này là bạn bắt đầu tìm nạp ngay lập tức trong chu kỳ kết xuất của thành phần cha mẹ và truyền một đối tượng đại diện cho việc tìm nạp cho trẻ em. Điều này ngăn bạn phải có một số loại đối tượng Rào chắn (mà bạn sẽ cần trong các phương pháp khác).

Tôi sẽ nói rằng tôi không nghĩ đợi cho đến khi mọi thứ được tải xuống để hiển thị mọi thứ là cách tiếp cận phù hợp vì khi đó UI sẽ chậm như kết nối chậm nhất hoặc có thể không hoạt động!

Đây là phần còn lại của mã bị thiếu:

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

Và một hộp cát: https://codesandbox.io/s/falling-dust-b8e7s


1
chỉ cần đọc các bình luận .. Tôi không tin rằng sẽ chờ đợi tất cả các thành phần được hiển thị trong DOM - và đó là một lý do chính đáng. Điều đó có vẻ rất hacky thực sự. Bạn đang cố gắng để đạt được điều gì?
Dan Pantry

1

Trước hết, vòng đời có thể có phương thức async / await.

Những gì chúng ta cần biết về componentDidMountcomponentWillMount,

thành phầnWillMount được gọi là cha mẹ đầu tiên sau đó con.
thành phầnDidMount làm ngược lại.

Theo tôi, chỉ cần thực hiện chúng bằng cách sử dụng vòng đời bình thường là đủ tốt.

  • thành phần con
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();
}
  • thành phần cha mẹ
// Initial state `loading: true`
componentDidUpdate() {
  this.checkIfResHaveBad(); // If all good, `loading: false`
}
...
{this.state.loading ? <CircularProgress /> : <YourComponent/>}

Tài liệu-UI tròn

Vì việc dựng lại con khiến cha mẹ cũng làm như vậy, nên bắt nó vào didUpdatesẽ ổn.
Và, vì nó được gọi sau khi kết xuất, nên các trang sẽ không bị thay đổi sau đó nếu bạn đặt chức năng kiểm tra theo yêu cầu của mình.

Chúng tôi sử dụng triển khai này trong sản phẩm của chúng tôi có api gây ra 30 giây để nhận được phản hồi, tất cả đều hoạt động tốt như tôi thấy.

nhập mô tả hình ảnh ở đây


0

Khi bạn thực hiện cuộc gọi api componentDidMount, khi cuộc gọi API sẽ giải quyết, bạn sẽ có dữ liệu componentDidUpdatevì vòng đời này sẽ vượt qua bạn prevPropsprevState. Vì vậy, bạn có thể làm một cái gì đó như dưới đây:

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

Nếu bạn muốn biết điều này trước khi kết xuất lại thành phần, bạn có thể sử dụng getSnapshotBeforeUpdate.

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


0

Có nhiều cách tiếp cận bạn có thể đi với:

  1. Cách dễ nhất là chuyển một cuộc gọi lại cho thành phần con. Các thành phần con này sau đó có thể gọi lại cuộc gọi này ngay khi chúng được kết xuất sau khi tìm nạp dữ liệu cá nhân của chúng hoặc bất kỳ logic nghiệp vụ nào khác mà bạn muốn đặt. Đây là một hộp cát cho cùng: https://codesandbox.io/s/unruffled-shockley-yf3f3
  2. Yêu cầu các thành phần phụ thuộc / con cập nhật khi chúng được thực hiện với việc hiển thị dữ liệu được tìm nạp sang trạng thái ứng dụng toàn cầu mà thành phần của bạn sẽ lắng nghe
  3. Bạn cũng có thể sử dụng React.useContext để tạo trạng thái cục bộ cho thành phần cha mẹ này. Các thành phần con sau đó có thể cập nhật bối cảnh này khi chúng được thực hiện với kết xuất dữ liệu được tìm nạp. Một lần nữa, thành phần cha mẹ sẽ lắng nghe bối cảnh này và có thể hành động tương ứng
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.