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 ไม่เพียงพอ ฉันต้องรู้ว่าเมื่อใดที่ post-data-fetching-render เสร็จสมบูรณ์และ DOM ได้รับการปรับปรุงอย่างสมบูรณ์เพื่อที่ฉันจะสามารถเปิดตัวโหลดเดอร์ได้
JasonGenX

คำตอบ:


5

มีสองวงจรชีวิตใน React ที่ถูกเรียกใช้หลังจาก DOM ของคอมโพเนนต์แสดงผลแล้ว:

สำหรับกรณีการใช้งานของคุณองค์ประกอบหลักของคุณPสนใจเมื่อNองค์ประกอบย่อยมีเงื่อนไขตามที่Xกำหนด X สามารถกำหนดเป็นลำดับ:

  • การดำเนินการ async เสร็จสมบูรณ์
  • คอมโพเนนต์ได้แสดงผลแล้ว

โดยการรวมสถานะของส่วนประกอบและการใช้componentDidUpdateเบ็ดคุณสามารถทราบเมื่อลำดับเสร็จสมบูรณ์และส่วนประกอบของคุณเป็นไปตามเงื่อนไข X

คุณสามารถติดตามเมื่อการดำเนินการ async ของคุณเสร็จสมบูรณ์โดยการตั้งค่าตัวแปรสถานะ ตัวอย่างเช่น:

this.setState({isFetched: true})

หลังจากตั้งค่าสถานะตอบสนองจะเรียกใช้componentDidUpdateฟังก์ชั่นส่วนประกอบของคุณ โดยการเปรียบเทียบวัตถุสถานะปัจจุบันและก่อนหน้าภายในฟังก์ชันนี้คุณสามารถส่งสัญญาณไปยังองค์ประกอบหลักที่การดำเนินการ async ของคุณเสร็จสมบูรณ์และสถานะขององค์ประกอบใหม่ของคุณแสดงผลแล้ว:

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ถูกทำเครื่องหมายเป็นเท็จ สิ่งนี้สามารถทำได้ แต่เรียงลำดับจากจุดของ 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>
  );
}

ตอนนี้ค่อนข้างจะใจจดใจจ่อจับคู่กับ 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

สิ่งที่เราต้องรู้เกี่ยวกับcomponentDidMountและcomponentWillMount,

componentWillMount ถูกเรียกว่า parent ก่อนจากนั้น child
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 ซึ่งทำให้เกิด 30s เพื่อรับการตอบสนองทั้งหมดทำงานได้ดีเท่าที่ฉันเห็น

ป้อนคำอธิบายรูปภาพที่นี่


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. วิธีที่ง่ายที่สุดคือการส่งกลับไปยังองค์ประกอบลูก คอมโพเนนต์ชายด์เหล่านี้สามารถเรียกการเรียกกลับนี้ได้ทันทีที่แสดงผลหลังจากดึงข้อมูลส่วนบุคคลหรือตรรกะทางธุรกิจอื่น ๆ ที่คุณต้องการวาง นี่คือ Sandbox สำหรับเดียวกัน: https://codesandbox.io/s/unruffled-shockley-yf3f3
  2. ขอให้คอมโพเนนต์ที่พึ่งพา / เด็กอัปเดตเมื่อดำเนินการเสร็จสิ้นด้วยการแสดงข้อมูลที่ถูกดึงกลับสู่สถานะแอปทั่วโลกซึ่งองค์ประกอบของคุณจะรับฟัง
  3. คุณยังสามารถใช้ React.useContext เพื่อสร้างสถานะที่แปลเป็นภาษาท้องถิ่นสำหรับองค์ประกอบพาเรนต์นี้ องค์ประกอบลูกสามารถปรับปรุงบริบทนี้เมื่อพวกเขาจะทำกับการแสดงผลข้อมูลที่ดึงมา อีกครั้งองค์ประกอบหลักจะฟังบริบทนี้และสามารถปฏิบัติตาม
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.