Rendering React Components with Promises Inside the Render Method

Rendering React components with promises inside the render method

render() method should render UI from this.props and this.state, so to asynchronously load data, you can use this.state to store imageId: imageUrl mapping.

Then in your componentDidMount() method, you can populate imageUrl from imageId. Then the render() method should be pure and simple by rendering the this.state object

Note that the this.state.imageUrls is populated asynchronously, so the rendered image list item will appear one by one after its url is fetched. You can also initialize the this.state.imageUrls with all image id or index (without urls), this way you can show a loader when that image is being loaded.

constructor(props) {
super(props)
this.state = {
imageUrls: []
};
}

componentDidMount() {
this.props.items.map((item) => {
ImageStore.getImageById(item.imageId).then(image => {
const mapping = {id: item.imageId, url: image.url};
const newUrls = this.state.imageUrls.slice();
newUrls.push(mapping);

this.setState({ imageUrls: newUrls });
})
});
}

render() {
return (
<div>
{this.state.imageUrls.map(mapping => (
<div>id: {mapping.id}, url: {mapping.url}</div>
))}
</div>
);
}

API call returns a promise instead of data, inside react component

so basically this is what is happening:

  • you have component 'X'
  • inside component 'X' you create a const data which uses an async function within it to generate some properties etc. Remember its async, but your react component is not. So the 'Promise' of this async function would not have resolved by the time your return statement starts rendering on your screen. So ultimately when your <Line/> component is rendered, data is still a pending Promise` and not really actual data.

There are a few other complexities to why your code is not working, one of them being the async await inside a .map which needs to be dealt with in a whole other way, using something called Promise.all, but what i have tired to explain above, is more of a concept on why your code is not working.

I'll try to give a simplified example that should explain the correct 'pattern' to do what you are doing:


import main from '../<some path>' //your current async function. assuming it does what its supposed to be doing

//this is how you deal with async in map.

const genFullData = async (labels, callback) => {
let promises = labels.map(async (year) => {
let result = await main(year, props.statSelection, 25);
return result;
}),

let dataResults = await Promise.all(promises);

let fullData = {
labels,
datasets: [
{
label: "25th percentile",
data: dataResults
},
],
};

callback(fullData);
}

const X = props => { //react comp

const [data, setData] = useState(undefined)

useEffect(() => {

genFullData(
props.labels,
(fullData) => setData(fullData)
)

},[props.labels])

return (
!data ? <h3>Loading...</h3> : <Line data={data} />
)

}

So ill explain the different parts of the above code:

  • First the async genFullData function. 2 things to notice. 1// see how async inside .map has to be dealt with using something called Promise.all read up on it. But in essence, Promise.all ensures that the function 'waits' until all the promises in the .map loop are resolved (or rejected), and only then moves on with the code. 2// Second thing to notice is, how in the end, once the Promises are done with, i take the results, shove them into the data structure that i picked up from your code, and then i feed this 'final data' to a callback function, which you'll understand in a bit.

  • Now lets look at the react component. //1 have initiated a local data state with a value of undefined //2 Inside a useEffect, I fire the genFullData function, and in the callback that i pass to it, i update the data state to whatever the genFullData function returns.

  • Finally the return statement of the comp renders a 'loading..' if the data state is undefined, and the minute the data state becomes 'defined', then the Line comp is rendered with the appropriate data being passed to it.

This last bit can be confusing so ill try to summarize again.

  1. comp renders for the first time.
  2. Init data state is set to undefined
  3. genAllData function is fired in a useEffect (which fires only once). This is an sync function, so it takes sometime till it actually manages to get the data.
  4. In the meantime, the react component continues rendering (remember, data has not been fetched yet by genAllData)
  5. React hits the return statement, where it checks to see if data is 'defined'. Well it is not, so it renders 'Loading..' on screen
  6. In the meantime, genAllData manages to successfully complete its operations, and the callback function is fired.
  7. In this callback, we setData to whatever genAllData is providing to the callback.
  8. This setData triggers the component to rerender.
  9. return statement fires again.
  10. this time data IS 'defined', so the Line comp is rendered, with the appropriate data being passed into it.

Rendering async promises through function components

You're right. The result of getUnitPrice() is a Promise, not a value, so what React does is it prints out the stringified version of that Promise. If you need the fulfilled value, you need a state value that will re-render the page if updated. Something like this:

const [price, setPrice] = useState('');

useEffect(() => {
getUnitPrice().then((p) => setPrice(p));
}, []);

...

<div>Price: {price}</div>

If you're using a class component, you can initialize the state the same way like this:

state = {
price: '',
}
async componentDidMount() {
const p = await getUniPrice();
this.setState({ price: p });
}

Render a simple list in React with promises

This code will handle the object that is returned by api and also moves the fetching to componentDidMount.

constructor(props) {
super(props);
this.state = {
data: [],
}
}
componentDidMount() {
firebaseCon.content.get('text', { fields: ['id', 'title'] })
.then((response) => {
let data = [];
for (item in response) {
data.push(response[item]);
}
this.setState({ data });
});
}

render() {
let itemList = this.state.data.map(function(item) {
return <li className="item" key={item.id}>{item.title}</li>;
});
return (
<ul>
{itemList}
</ul>
)
}

A closer look at Promises' methods then and catch should make it clearer: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then

How to re-render React Component when promise resolves? | How to block render until data loads?

Writing a functional react component is simple with the new React Hooks. In the example below, I'm using useState and useEffect. The useState hook is synonymous with this.state/this.setState in a class-based React component. The useEffect hook is similar to componentDidMount+componentDidUpdate. It also is capable of being componentDidUnmount.

The way the code will execute is from top to bottom. Because it's functional, it will run through once and render with the default state set at the argument to useState. It will not block on getting data from the API in the useEffect function. Thus, you need to be able to handle loading without having data. Anytime props.apiConfig or props.id changes, the component will re-render and all the useEffect again. It will only call useEffect if props.apiConfig and props.id do change after first run. The only nasty part is that useEffect cannot be an async function, so you have to call the function getDataWrapper without using await. When the data is received by the API, it will store the data in state, which will trigger a re-render of the component.

To Summarize:

  1. Render once with default state

    • Call useEffect, which calls getDataWrapper
    • return component with initial values in useState
  2. Once data is received by the API in the useEffect/getDataWrapper function, set the state via setState & set isLoading to false
  3. Re-render the component with updated value that setState now contains

    • Avoid the useEffect control path since the values in the second argument of useEffect have not changed. (eg: props.apiConfig & props.id).
import React, { useState, useEffect } from 'react';
import { getDataFromAPI } from './api';

const MyComponent = (props) => {

const [state, useState] = useState({
isLoading: true,
data: {}
});

useEffect(() => {
const getDataWrapper = async () => {
const response = await getDataFromAPI(apiConfig, props.id);
setState({
isLoading: false,
data: response
});
});

getDataWrapper();
}, [props.apiConfig, props.id]);

if(state.isLoading) { return <div>Data is loading from API...</div>

return (
<div>
<h1>Hello, World!</h1>
<pre>{JSON.stringify(state.data, null, 2)}</pre>
</div>
);
};

export default MyComponent;

How can I display the result of a promise on a webpage in a react export

The render method of all React components is to be considered a pure, synchronous function. In other words, there should be no side effects, and no asynchronous logic. The error Error: Objects are not valid as a React child (found: [object Promise]) is the component attempting to render the Promise object.

Use the React component lifecycle for issuing side-effects. componentDidMount for any effects when the component mounts.

class App extends Component {
state = {
athena: null,
}

componentDidMount() {
athena.then(result => this.setState({ athena: result }));
}

render() {
const { athena } = this.state;
return athena;
}
}

If you needed to issue side-effects later after the component is mounted, then componentDidUpdate is the lifecycle method to use.

Class components are still valid and there's no plan to remove them any time soon, but function components are really the way going forward. Here's an example function component version of the code above.

const App = () => {
const [athenaVal, setAthenaVAl] = React.useState(null);

React.useEffect(() => {
athena.then(result => setAthenaVAl(result));
}, []); // <-- empty dependency array -> on mount/initial render only

return athenaVal;
}

The code is a little simpler. You can read more about React hooks if you like.



Related Topics



Leave a reply



Submit