Rendering component before fetching data has been finished - javascript

I'm trying to fetch data by creating a function. In that function I am doing trying to set state and I am calling it from the componentDidMount method, but I am having a few problems:
I am not sure if while is good practice to be used, because I am looping and changing my endpoint so I can get new data every time.
I have tried to return data from the fetching function and use setState inside componentDidMount, but I had a problem, I suspect because componentDidMount is running before fetching has completed
I have tried to use res.json() on the data using a promise, but I got an error that res.json is not a function.
state = {
title: [],
image: [],
rating: [],
};
getData = () => {
let i = 1;
while (i <= 9) {
axios.get(`http://api.tvmaze.com/shows/${i}`)
.then(response => console.log(response))
.then(response => this.setState({
title:response.data.data.name[i],
}))
.catch(error => console.log(error));
i++;
}
};
componentDidMount() {
this.getData();
console.log(this.state.title);
}

If your goal is to render your JSX after you're done fetching information, then I'd suggest creating an additional item in your state, isLoading, that you can set to true or false and render your JSX conditionally.
Based on the example you provided below, it'd look like the following:
class Shows extends React.Component {
state = {
title: [],
image: [],
rating: [],
isLoading: true
}
componentDidMount() {
this.getData()
}
getData = () => {
// I've created a URL for each request
const requestUrls = Array.from({ length: 9 })
.map((_, idx) => `http://api.tvmaze.com/shows/${idx + 1}`);
const handleResponse = (data) => {
// `data` is an array of all shows that you've requested
// extract information about each show from the payload
const shows = data.map(show => show.data)
// handle shows data however you need it
// and don't forget to set `isLoading` state to `false`
this.setState({
isLoading: false,
title: shows.map(show => show.name),
image: shows.map(show => show.url),
rating: shows.map(show => show.rating.average),
})
}
const handleError = (error) => {
// handle errors appropriately
// and don't forget to set `isLoading` to `false`
this.setState({
isLoading: false
})
}
// use `Promise.all()` to trigger all API requests
// and resolve when all requests are completed
Promise.all(
requestUrls.map(url => axios.get(url))
)
.then(handleResponse)
.catch(handleError)
}
render() {
const { isLoading, title, image, rating } = this.state
// prevent showing your `JSX` unless data has been fetched
// ideally, show a loading spinner or something that will
// tell users that things are happening;
// returning `null` won't render anything at all
if (isLoading) {
return null
}
return (
<div>...</div>
)
}
}
This way, with Promise.all, it's a lot easier to reason about all these calls that you're making.
Other than that, using componentDidMount to fetch data from an API is the right place to do it, but I'd stay away from the while loop and use Promise.all for all your requests and map to create an array of promises (requests) that can be passed to Promise.all and handled all at once.
Working example:
CodeSandbox

The way in which you are setting state will result in the last data from api to be saved in state and it will render only last call
Do it like this
getData = () => {
let i = 1;
while (i <= 9) {
axios.get(`http://api.tvmaze.com/shows/${i}`)
.then(response =>{
let prevState=this.state.title
prevState.push(response.data.data.name[i])
this.setState({
title:prevState,
})})
.catch(error => console.log(error));
i++;
}
};

Related

PokeAPI - Fetching Pokemon Data

I have just started to learn React and I am trying to build a PokeDex using the PokeAPI, but I am having an extremely hard time understanding how to fetch data from the API. I want to fetch the first 384 Pokemon related information. I have written this code below in componentDidMount() to extract the complete data in my main App class component, and push it to an array called pokemonArray which I will set to my state.
let pokemonArray = [];
/* First API fetch call to return names and URL's of first 384 Pokemon after promise is resolved.*/
fetch('https://pokeapi.co/api/v2/pokemon/?limit=384')
.then(response => response.json())
.then(data => {
let results = data.results;
let promisesArray = results.map(result => {
return fetch(result.url).then(response => response.json()).then(data => pokemonArray.push(data));
})
return Promise.all(promisesArray)
}).then(this.setState({ pokemon: pokemonArray }, () => console.log('Main Pokemon State: ', this.state.pokemon)));
}
In my render method I want to pass this newly set state as a prop to a component called PokeList like so
<PokeList pokemon={this.state.pokemon} />
Once I pass it on and I try to render it out in my PokeList component like so
export const PokeList = ({ pokemon }) => {
console.log(pokemon);
return (
<div className="pokecard-container">
<h1>{pokemon[0].id}</h1>
</div>
);
}
I get an error that says TypeError: Cannot read property '0' of null. The React Developer tools shows the state being populated with the retrieved values as well as the props being set as well, but it seems to consider it null. Could anyone please help out with this, it's been so frustrating to see this error out
There are two issues with your code. First, here's a modified working version:
fetch('https://pokeapi.co/api/v2/pokemon/?limit=2')
.then(response => response.json())
.then(data => {
let results = data.results;
let promisesArray = results.map(result => {
return fetch(result.url).then(response => response.json());
})
return Promise.all(promisesArray);
}).then((data) => this.setState({ pokemon: data }, () => console.log('Main Pokemon State: ', this.state.pokemon)));
And the explanation:
Issue 1:
Original code:
let promisesArray = results.map(result => {
return fetch(result.url).then(response => response.json()).then(data => pokemonArray.push(data));
})
return Promise.all(promisesArray)
The last then() callback returns the results of pokemonArray.push(data). Array.push returns the length of the array, so return Promise.all(promisesArray) returns an array of array lengths.
Change it to:
let promisesArray = results.map(result => {
return fetch(result.url).then(response => response.json()); // Returns the data
})
return Promise.all(promisesArray)
Issue 2:
Original code:
then(this.setState({ pokemon: pokemonArray }, () => console.log('Main Pokemon State: ', this.state.pokemon)));
You must supply a callback function to then():
.then((data) => this.setState({ pokemon: data }, () => console.log('Main Pokemon State: ', this.state.pokemon)));
You must also remember to initialize the state when your component is created:
constructor(props) {
super(props);
this.state = {
pokemon: [],
};
}
Thus, when you render PokeList (<PokeList pokemon={this.state.pokemon} />), it will always receive an array, though it will be empty until the data is fetched.
Here is my suggestion to rewrite your fetching code :
class App extends Component{
state = {
pokemonArray : []
}
componentDidMount = () =>{
fetch('https://pokeapi.co/api/v2/pokemon/?limit=384')
.then(response => response.json())
.then(data => {
let results = data.results;
this.setState({ pokemonArray : [...results]})
console.log("Here are my pokemons", this.state.pokemonArray)
})
}
...
}
I'm using spread operator to copy the received data to the pokemonArray. You could pass then your array to the component as a prop.
Hope this helps you.

Error when using a function that requests API data

I'm working on this app that takes data from a movies API and I want to work with it.
I have this function that gets the API data:
/** #format */
const fetchMovie = movie => {
var APIKEY = "xxxxxxxxxxxxxxxxxxxxxx";
var API2 =
"https://api.themoviedb.org/3/search/movie?api_key=xxxxxxxxxx&language=en-US&page=1&include_adult=false&query=avengers";
var API = `https://api.themoviedb.org/3/search/movie?api_key=${APIKEY}&language=en-US&page=1&query=${movie}`;
fetch(API2)
.then(data => data.json())
.then(movies => console.log(movies) || movies.items)
.catch(error => {
console.log(error);
return null;
});
};
export default fetchMovie;
And I have this App class that uses the API data:
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
activeMovie: "Avengers",
loading: true,
allMovies: []
};
}
componentDidMount() {
this.getData(this.activeMovie);
}
componentDidUpdate(prevState) {
if (prevState.activeMovie !== this.state.activeMovie) {
this.getData(this.state.activeMovie);
}
}
getData(movie) {
this.setState({
loading: true
});
fetchMovie(movie).then(data => {
this.setState({
allMovies: data,
loading: false
});
});
}
Now, before this I have used the same methodology and it worked but I don't know why the I get
TypeError: Object(...)(...) is undefined // this line fetchMovie(movie).then(data => {
The API is good, I can console log the data before it gets to the App component, but the function in the app component somehow doesn't work. any clues?
That's simply because your function fetchMovie() doesn't return a Promise so that you than use .then() after it. You can return a promise instead. However the logic in your code is probably a bit shaky. You might as well look that up because it goes into an infinite loop, consider debugging component life cycles for that.
To return a promise from your function, you can use a similar approach as I wrote in here: https://codesandbox.io/s/small-sun-sfcyv.
You are not returning any promise from your fetchMovie function, that way you can't use the .then so right now you only have access to that data in your fetchMovie. A possible solution would be defining your function as async and then you would be able to return your data from that function.
Try this.
/** #format */
const fetchMovie = movie => {
var APIKEY = "xxxxxxxxxxxxxxxxxxxxxx";
var API2 =
"https://api.themoviedb.org/3/search/movie?api_key=xxxxxxxxxx&language=en-US&page=1&include_adult=false&query=avengers";
var API = `https://api.themoviedb.org/3/search/movie?api_key=${APIKEY}&language=en-US&page=1&query=${movie}`;
return fetch(API2)
.then(data => data.json())
.then(movies => console.log(movies) || movies.items)
.catch(error => {
console.log(error);
return null;
});
};
export default fetchMovie;

Using React.setState in componentDidMount() for data returned within nested promises?

I'm trying to put some data into state in a React app. The flow involves fetching a list of IDs from the HackerNews API, then taking each ID and making an additional API call to fetch the item associated with each ID. I ultimately want to have an array of 50 items in my component state (the resulting value of the each '2nd-level' fetch.
When I setState from JUST the single 'top-level' promise/API call, it works fine and my state is set with an array of IDs. When I include a second .then() API call and try to map over a series of subsequent API calls, my state gets set with unresolved Promises, then the fetch() calls are made.
I'm sure this a problem with my poor grasp on building appropriate async methods.
Can someone help me figure out what I'm doing wrong, and what the best practice for this is??
My component:
import React from 'react'
import { fetchStoryList } from '../utils/api'
export default class Stories extends React.Component {
state = {
storyType: 'top',
storyList: null,
error: null,
}
componentDidMount () {
let { storyType } = this.state
fetchStoryList(storyType)
.then((data) => {
console.log("data", data)
this.setState({ storyList: data })
})
.catch((error) => {
console.warn('Error fetching stories: ', error)
this.setState({
error: `There was an error fetching the stories.`
})
})
}
render() {
return (
<pre>{JSON.stringify(this.state.storyList)}</pre>
)
}
}
My API Interface:
// HackerNews API Interface
function fetchStoryIds (type = 'top') {
const endpoint = `https://hacker-news.firebaseio.com/v0/${type}stories.json`
return fetch(endpoint)
.then((res) => res.json())
.then((storyIds) => {
if(storyIds === null) {
throw new Error(`Cannot fetch ${type} story IDs`)
}
return storyIds
})
}
function fetchItemById(id) {
const endpoint = `https://hacker-news.firebaseio.com/v0/item/${id}.json`
return fetch(endpoint)
.then((res) => res.json())
.then((item) => item)
}
export function fetchStoryList (type) {
return fetchStoryIds(type)
.then((idList) => idList.slice(0,50))
.then((idList) => {
return idList.map((id) => {
return fetchItemById(id)
})
})
//ABOVE CODE WORKS WHEN I COMMENT OUT THE SECOND THEN STATEMENT
You are not waiting for some asynchronous code to "finish"
i.e.
.then((idList) => {
return idList.map((id) => {
return fetchItemById(id)
})
})
returns returns an array of promises that you are not waiting for
To fix, use Promise.all
(also cleaned up code removing redundancies)
function fetchStoryIds (type = 'top') {
const endpoint = `https://hacker-news.firebaseio.com/v0/${type}stories.json`;
return fetch(endpoint)
.then((res) => res.json())
.then((storyIds) => {
if(storyIds === null) {
throw new Error(`Cannot fetch ${type} story IDs`);
}
return storyIds;
});
}
function fetchItemById(id) {
const endpoint = `https://hacker-news.firebaseio.com/v0/item/${id}.json`
return fetch(endpoint)
.then(res => res.json());
}
export function fetchStoryList (type) {
return fetchStoryIds(type)
.then(idList => Promise.all(idList.slice(0,50).map(id => fetchItemById(id)));
}
One solution would be to update fetchStoryList() so that the final .then() returns a promise that is resolved after all promises in the mapped array (ie from idList.map(..)) are resolved.
This can be achieved with Promise.all(). Promise.all() take an array as an input, and will complete after all promises in the supplied array have successfully completed:
export function fetchStoryList(type) {
return fetchStoryIds(type)
.then((idList) => idList.slice(0,50))
.then((idList) => {
/* Pass array of promises from map to Promise.all() */
return Promise.all(idList.map((id) => {
return fetchItemById(id)
});
});
}

React state in render is unavailable inside return

I have these methods that do some fetching, and then once done, they set the state. But the render is called before the state is done and does not update.
The below seems to work on it's own, but takes a minute to finish.
//returns an promise with Array
getTopIDs(url) {
return fetch(url).then(blob => blob.json()).then(json => json)
}
// makes a URL fetchs JSON and return promise with single ID
getStory(id) {
let url = `https://hacker-news.firebaseio.com/v0/item/${id}.json?print=pretty`
return fetch(url).then(blob => blob.json()).then(json => json)
}
// call above methods, set state when done
componentDidMount() { //
let arr = []
let promise = new Promise((resolve, reject) => {
let data = this.getTopIDs("https://hacker-news.firebaseio.com/v0/topstories.json?print=pretty").then((idArr) => {
idArr.forEach((id, index) => {
this.getStory(id).then(res => {
arr.push(res)
})
})
//resolve once all pushed to arr
resolve(arr)
})
})
// set state once array is completed
promise.then(res => {
return this.setState({data: arr})
})
}
Then in the render below it logs 'no', 'no' and stops. Trying it outside the return it logs 'no','yes'. Searching other posts for this I tried setting a boolean when done and using the state callback but those did not work (full disclosure: I don't really understand the setState callback option)
render() {
return (
<div>
{
this.state.data.length
? console.log('yes')
: console.log('no')
}
</div>)
}
I need render to handle this.state.data only when done. How can I do it?
Add fiddle: https://jsfiddle.net/drumgod/e2atysu3/6/
Your method this.getStory() is async but your handling of the array creation is sync inside your promise.
You need to either use async/await or only run your resolve(arr) after idArr.forEach() is for sure completed (which may be easier to do using Promise.all(idArr.map(...)) where the ... is returning the result from this.getStory()).
This is how you'll want to set your state inside getStory:
this.setState(prevState => ({
data: [...prevState.data, res]
}))
As mentioned in the comments, this would render the component for each data point in the forEach.
In order to avoid this issue, this is how componentDidMount() should be formatted:
componentDidMount() {
const arr = [];
this.getTopIDs("https://hacker-news.firebaseio.com/v0/topstories.json?print=pretty").then((idArr) => {
idArr.forEach((id, index) => this.getStory(id).then(res => arr.push(res)));
this.setState(prevState => ({ data: [...prevState.data, arr] }))
})
}
This also lets you get rid of the promise.then call at the end.

Asynchronous ActionCreator in React Redux

I'm pretty new in React-Redux. Was working on an application. The thing is that I faced some issues with asynchronous execution of Redux actionCreator, may be.
Below is my component. Say, I want to call an actionCreator from componentDidMount() or from an onclick event listener.
class Dashboard extends PureComponent {
componentDidMount() {
this.props.getProductsAndPackages();
let something = [];
something = this.props.products;
}
....................................
}
Or , the function this.props.getProductsAndPackages(); can be an onClick event handler that does the same thing, context is the same. I'll ask my question after first explaining my code.
At the lower side of my Dashboard container:
Dashboard.propTypes = {
getProductsAndPackages: PropTypes.func.isRequired,
products: PropTypes.array.isRequired,
.......................
};
const mapStateToProps = (state) => {
return {
.....................
products: state.products.products,
...................
};
};
const mapDispatchToProps = (dispatch) => {
return {
getProductsAndPackages: () => dispatch(getProductsAndPackagesActionCreator()),
};
};
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Dashboard));
My actionCreator goes like:
export const getProductsAndPackagesActionCreator = () => {
return (dispatch) => {
dispatch(productsIsLoading(true));
let url = 'xyz';
if(!!localStorage.getItem('_token')) {
const local_token = localStorage.getItem('_token');
const fullToken = 'Bearer '.concat(local_token);
axios.get(url, {headers: {Authorization: fullToken}})
.then(response => {
dispatch(productsIsLoading(false));
if (response.data.statusCode === 200) {
dispatch(productsFetched(true));
dispatch(products(response.data.data));
} else {
dispatch(productsFetched(false));
dispatch(productsErrors(response.data.message));
}
})
.catch(error => {
});
} else {
axios.get(url)
.then(response => {
dispatch(productsIsLoading(false));
if (response.data.statusCode === 200) {
dispatch(productsFetched(true));
dispatch(products(response.data.data));
} else {
dispatch(productsFetched(false));
dispatch(productsErrors(response.data.message));
}
})
.catch(error => {
console.log(error);
dispatch(productsIsLoading(false));
dispatch(productsErrors(error.message));
});
}
};
};
Now, I want my getProductsAndPackagesActionCreator() to return a Promise or anything that would allow my something variable to get the actual data returned from the server. Right now, by the time I'm getting actual data, the line something=this.props.products has already been executed and I get back the initialValue that was set for products.
I know, whenever I'll receive the populated products, component will re-render, but that does not help my decision making.
I'm using redux-thunk, by the way.
What should I do now ? Sorry for such a long post.
Actually I wanted getProductsAndPackagesActionCreator() to return a promise, which was pretty straightforward, to be honest. I figured out that if you just return the axios.get() or axios.post(), it will return a promise. So, the modified code looked like below:
export const getProductsAndPackagesActionCreator = () => {
return (dispatch) => {
dispatch(productsIsLoading(true));
let url = 'xyz';
if(!!localStorage.getItem('_token')) {
return axios.get(url, {headers: {Authorization: fullToken}})
.then(response => {
............
............
})
.catch(error => {
});
} else {
return axios.get(url)
.then(response => {
...........
...........
})
.catch(error => {
console.log(error);
});
}
};
};
And then, I could do something like below in componentDidMount() or on any onClick event:
this.props.getProductsAndPackages().then(() => {
this.setState({
...this.state,
clicked_product: this.props.product_by_id
}, () => {
//do other stuffs
});
});
Feel free to let me know if there's any issue.
I think you are close to getting what you want. First of all, you should understand that redux actions and react actions like setState are asynchronous, so you have to apply your logic keeping this in mind. I'm going to explain what i think in some points:
You have called the action creator in the correct place componentDidMount, also you can call this action in any onClick if you want.
As soon as you dispatch the action you are changing your redux state setting loading true I suppose. So now you can access this property in your render function, so you can render a Loader until your api call finishes.
When your ajax function finishes, with an error or not, I suppose you are setting loading to false and updating your products data, so you can render now your loaded products in your dashboard.
Are you sure that you have to compare your empty products array with the received data? Maybe you can check in your render function if (!this.props.products.length) return null, when you load your page you will see a loader function and later your dashboard with the products.
If you really need to compare previous products with received products componentDidUpdate is your method. In this method, you can access your previous props and compare with actual props, be careful comparing arrays, remember [] === [] is false. Maybe you can compare the length, something like
componentDidUpdate(prevProps){
if(prevProps.products.length !=== this.props.products.lenth){
doSomething()
}
}
Just to say that componentDidUpdate is executed after render, so be careful with your code to no-execute extra renderings.
Hope it helps, if you dont understand anyting just tell me :)

Categories

Resources