Reputation: 5434
TL;DR
How can a parent component know when the rendering of every child component under it has finished and the DOM visible to the user with its most up to date version?
Let's say I have a component A
that has a child Grid
component consisting of 3x3
grandchild components. Each of those grandchild components fetches data from a restful API endpoint and renders itself when the data becomes available.
I would like to cover the entire area of Component A
with a loader
placeholder, to be unveiled only when the last of the components in the grid has fetched the data successfully, and rendered it, such that it's already on the DOM and can be viewed.
The user experience should be a super smooth transition from "loader" to a fully populated grid without flickering.
My problem is knowing exactly when to unveil the components under the loader.
Is there any mechanism I can rely on to do this with absolute accuracy? I don't to hard code a time limit for the loader. As I understand relying on ComponentDidMount
for every child is also unreliable as it doesn't actually guarantee the component is fully visible to the user at the time of the call.
To distill the question even further:
I have a component that renders some kind of data. After it's initialized it doesn't have it, so in its
componentDidMount
it hits an API endpoint for it. Once it receives the data, it's changing its state to reflect it. This understandably causes a re-render of the final state of that component. My question is this: How do I know when that re-render has taken place and is reflected in the User facing DOM. That point in time != the point in time when the component's state has changed to contain data.
Upvotes: 18
Views: 14428
Reputation: 670
There are multiple approaches you can go with:
Upvotes: 1
Reputation: 15146
First of all, life-cycle could have async/await method.
What we need to know about componentDidMount
and componentWillMount
,
componentWillMount is called first parent then child.
componentDidMount do the opposite.
In my consideration, simply implement them using normal life-cycle would be good enough.
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
Since child re-rendering causing parent to do the same, catch it in didUpdate
would be fine.
And, since it's called after the rendering, the pages shouldn't be changed after that if you set the check function as your demand.
We use this implementation in our prod which has an api causing 30s to get the response, all worked well as far as I see.
Upvotes: 1
Reputation: 1881
There are two lifecycle hooks in React that are called after a component's DOM has rendered:
For your use case your parent component P is interested when N child components have each satisfied some condition X. X can be defined as a sequence:
By combining the state of the component and using the componentDidUpdate
hook, you can know when the sequence has completed and your component meets condition X.
You can keep track of when your async operation has completed by setting a state variable. For example:
this.setState({isFetched: true})
After setting state, React will call your components componentDidUpdate
function. By comparing the current and previous state objects within this function you can signal to the parent component that your async operation has completed and your new component's state has rendered:
componentDidUpdate(_prevProps, prevState) {
if (this.state.isFetched === true && this.state.isFetched !== prevState.isFetched) {
this.props.componentHasMeaningfullyUpdated()
}
}
In your P component, you can use a counter to keep track of how many children have meaningfully updated:
function onComponentHasMeaningfullyUpdated() {
this.setState({counter: this.state.counter + 1})
}
Finally, by knowing the length of N you can know when all meaningful updates have occurred and act accordingly in your render method of P:
const childRenderingFinished = this.state.counter >= N
Upvotes: 8
Reputation: 10538
You could potentially solve this using React Suspense.
The caveat is that it's not a good idea to Suspend past the component tree that does the render (that is: If your component kicks off a render process, it's not a good idea to have that component suspend, in my experience), so it's probably a better idea to kick the requests off in the component that renders the cells. Something like this:
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>
);
}
Now, quite how Suspense pairs with Redux I'm not sure. The whole idea behind this (experimental!) version of Suspense is that you start the fetch immediately during the render cycle of a parent component and pass an object that represents a fetch to the children. This prevents you having to have some kind of Barrier object (which you would need in other approaches).
I will say that I don't think waiting until everything has fetched to display anything is the right approach because then the UI will be as slow as the slowest connection or may not work at all!
Here's the rest of the missing code:
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>
);
}
And a sandbox: https://codesandbox.io/s/falling-dust-b8e7s
Upvotes: 2
Reputation: 10686
I would set it up so that you're relying a global state variable to tell your components when to render. Redux is better for this scenario where many components are talking to each other, and you mentioned in a comment that you use it sometimes. So I'll sketch out an answer using Redux.
You'd have to move your API calls to the parent container, Component A
. If you are wanting to have your grandchildren render only after the API calls complete, you can't keep those API calls in the grandchildren themselves. How can an API call be made from a component that doesn't exist yet?
Once all the API calls are made, you can use actions to update a global state variable containing a bunch of data objects. Every time data is recieved (or an error is caught), you can dispatch an action to check if your data object is fully filled out. Once its completely filled out, you can update a loading
variable to false
, and conditionally render your Grid
component.
So for example:
// 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 })
Your reducer willhold the global state object which will say if things are all ready to go yet or not:
// 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
}
}
}
In your actions:
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
}
}
}
Aside
If you are really married to the idea that your API calls live inside each grandchild, but that the whole Grid of grandchildren does not render until all API calls are completed, you'd have to use a completely different solution. In this case, your grandchildren would have to be rendered from the start to make their calls, but have a css class with display: none
, which only changes after the global state variable loading
is marked as false. This is also doable, but sort of besides the point of React.
Upvotes: 1
Reputation: 1882
As you are making an api call in componentDidMount
, when the API call will resolve you will have the data in componentDidUpdate
as this lifecycle will pass you prevProps
and prevState
. So you can do something like below:
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>
}
}
If you want to know this before the component re renders, you can use getSnapshotBeforeUpdate
.
https://reactjs.org/docs/react-component.html#getsnapshotbeforeupdate.
Upvotes: 0