[reactjs] 반응-부모 구성 요소의 모든 하위 구성 요소가 사용자에게 언제 표시되는지 감지하는 방법은 무엇입니까?
TL; DR
부모 구성 요소는 그 아래의 모든 자식 구성 요소 렌더링이 완료되고 DOM이 최신 버전으로 사용자에게 표시되는 시점을 어떻게 알 수 있습니까?
손자 구성 요소로 구성된 component A
자식 Grid
구성 요소 가 있다고 가정 해 봅시다 3x3
. 각 손자 구성 요소는 편안한 API 끝점에서 데이터를 가져오고 데이터를 사용할 수있게되면 자체적으로 렌더링됩니다.
나는의 전체 영역을 커버하고 싶은 Component A
로모그래퍼 loader
는 DOM에 이미하고 볼 수 있도록 그리드의 구성 요소의 마지막이 성공적으로 가져온 데이터가있는 경우에만 공개하고 렌더링 할 자리 표시 자.
사용자 경험은 깜박 거림없이 “로더”에서 완전히 채워진 그리드로 매우 부드럽게 전환되어야합니다.
내 문제는 로더 아래에서 구성 요소를 언제 공개 해야하는지 정확히 알고 있습니다.
절대 정확도 로이 작업을 수행 할 수있는 메커니즘이 있습니까? 로더에 대한 시간 제한을 하드 코딩하지 않습니다. 내가 ComponentDidMount
전화를 할 때 사용자가 구성 요소를 완전히 볼 수 있다고 보장하지는 않으므로 모든 어린이에게 의존하는 것도 신뢰할 수 없습니다.
질문을 더 증류하려면 :
일종의 데이터를 렌더링하는 구성 요소가 있습니다. 초기화 된 후에는 그것을 가지고 있지 않으므로
componentDidMount
API 엔드 포인트에 도달합니다. 데이터를 수신하면 상태를 반영하여 데이터를 반영합니다. 이것은 당연히 해당 구성 요소의 최종 상태를 다시 렌더링하게합니다. 내 질문은 이것입니다 : 다시 렌더링이 언제 발생 하고 사용자 직면 DOM에 반영 되는지 어떻게 알 수 있습니까 ? 해당 시점! = 구성 요소의 상태가 데이터를 포함하도록 변경된 시점.
답변
컴포넌트의 DOM이 렌더링 된 후 호출되는 두 가지 라이프 사이클 후크가 React에 있습니다.
- componentDidMount
- componentDidUpdate (이것에 관심이 있습니다)
유스 케이스의 경우 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
답변
구성 요소에 렌더링시기를 알리기 위해 전역 상태 변수를 사용하도록 설정했습니다. 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의 관점 이외에도 가능합니다.
답변
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>
);
}
Suspense가 Redux와 어떻게 연결되는지 잘 모르겠습니다. 이 Suspense 버전의 배후에있는 아이디어는 부모 구성 요소의 렌더링주기 동안 즉시 가져 오기를 시작하고 가져 오기를 나타내는 객체를 자식에게 전달한다는 것입니다. 이것은 당신이 어떤 종류의 장벽 객체를 가질 필요가 없도록합니다 (다른 접근법에 필요할 것입니다).
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
답변
우선, 라이프 사이클 에는 비동기 / 대기 방법이있을 수 있습니다.
우리가 알아야 할 사항 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/>}
머티리얼 -UI CircularProgress
자식을 다시 렌더링하면 부모가 똑같이 didUpdate
할 수 있기 때문에 잘 잡을 수 있습니다.
그리고 렌더링 후에 호출되기 때문에 check 기능을 요구 사항으로 설정하면 그 후에 페이지를 변경해서는 안됩니다.
우리는이 구현을 우리의 제품에서 30 대가 응답을 얻도록 API를 가지고 있으며 모두 내가 본 한 잘 작동했습니다.
답변
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 .
답변
여러 가지 접근 방식이 있습니다.
- 가장 쉬운 방법은 콜백을 자식 구성 요소에 전달하는 것입니다. 그런 다음 이러한 하위 구성 요소는 개별 데이터 또는 원하는 다른 비즈니스 로직을 가져온 후 렌더링하자마자이 콜백을 호출 할 수 있습니다. https://codesandbox.io/s/unruffled-shockley-yf3f3 의 샌드 박스는 다음과 같습니다.
- 페치 된 데이터를 컴포넌트가 수신하는 글로벌 앱 상태로 렌더링하여 업데이트가 완료되면 종속 / 자식 컴포넌트에 업데이트하도록 요청하십시오.
- React.useContext를 사용하여이 상위 컴포넌트의 현지화 된 상태를 작성할 수도 있습니다. 그런 다음 페치 된 데이터 렌더링이 완료되면 하위 컴포넌트가이 컨텍스트를 업데이트 할 수 있습니다. 다시 말하지만 부모 구성 요소는이 컨텍스트를 듣고 적절하게 작동 할 수 있습니다