PokeAPI - Fetching Pokemon Data - javascript

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.

Related

Update a Table with React Hooks when a Row is Added, Deleted and Modified? [Issue: Gets Before Post and Delete]

I'm using Axios to get, put, and delete values from our database and have them displayed in a table; however, I need to refresh the page to see my changes. To find answers, I've visited these posts: How to update the page after call Axios Successful ? React, refresh table after action in react, and How can I use React Hooks to refresh a table when a row is deleted or added?
Unfortunately, I am still stuck and unsure how to dynamically update table rows upon response updates.
Update: I have noticed that the getValues function runs prior to the post and delete methods, which is why it is currently showing the previous values before the methods execute.
Axios.js - Where I am using get and delete methods. They work as I've had responses printed on the console.
import axios from "axios";
const getValues = async () => {
const values = await axios
.get("https://pokeapi.co/api/v2/type/")
.then((response) => {
return response.data;
})
.catch(function (error) {
console.log(error);
});
return values;
};
const postValues = (values) => {
axios
.post("https://pokeapi.co/api/v2/type/")
.then((response) => {
console.log("Post Values: ", response.data);
return response.data;
});
};
const deleteValues = (id) => {
console.log(id);
const deleteValues = axios
.delete(`https://pokeapi.co/api/v2/type/${id}`)
.then((response) => {
console.log("Delete Values: ", response);
})
.catch(function (error) {
console.log(error);
});
return deleteValues;
};
export { getValues, postValues, deleteValues }
ValuesTable.js - Where the delete method executes
import Axios from "./Axios";
const [data, setData] = React.useState();
useEffect(() => {
Axios.getValues().then((result) => {
setData(result.data);
});
}, [data]);
return (
{data.map((values) => {
<TableRow/>
<TableCell>{values.values}</TableCell>
<TableCell>
<Button
onClick={() =>
Axios.deleteValues(values.id);
}
/>
})};
)
Form.js - Where the post method executes
if (values.id === 0) {
Axios.postValues(values);
} else {
Axios.putValues(values, values.id);
}
UseState setData(result.data) loads all the existing values in the database.
Method deleteValues deletes a value in an array.
Method postValues adds a value into the database.
Well, you don't what to unconditionally call setData within an useEffect hook with data as a dependency as this will cause an infinite loop (render looping) to occur.
Since the getValues utility already unpacks the response.data value there is likely no need to do it again in your UI. Also, remove the data dependency.
useEffect(() => {
Axios.getValues()
.then((result) => {
setData(result.results);
});
}, []);
For the deleteValues utility, if console.log("Delete Values: ", response); is showing the correct values than I think you need to return this value from deleteValues.
const deleteValues = (id) => {
console.log(id);
const deleteValues = axios
.delete("https://pokeapi.co/api/v2/type/${id}`)
.then((response) => {
console.log("Delete Values: ", response);
return response; // <-- new data values
})
.catch(function (error) {
console.log(error);
});
return deleteValues;
};
Then in ValuesTable you need to update your data state with the new deleted values.
{data.map((values) => {
...
<Button
onClick={() => {
Axios.deleteValues(values.id)
.then(data => setData(data));
}}
/>
...
})};
Update
Ok, since the deleteValues utility doesn't return the updated data from the backend you will need to maintain your local state manually. I suggest doing this work in a callback handler. Upon successful deletion, update the local state.
const [data, setData] = React.useState();
useEffect(() => {
Axios.getValues().then((result) => {
setData(result.data);
});
}, []);
const deleteHandler = id => async () => {
try {
await Axios.deleteValues(id); // no error, assume success
setData(data => data.filter((item) => item.id !== id));
} catch(err) {
// whatever you want to do with error
}
};
return (
...
{data.map((values) => {
<TableRow/>
<TableCell>{values.values}</TableCell>
<TableCell>
<Button onClick={deleteHandler(values.id)}>
Delete
</Button>
})};
...
)
Note that I've written deleteHandler to be a curried function so you don't need an anonymous callback function for the button's onClick handler. It encloses the current id in an "instance" of the callback.
Update 2
If you are making a lot of different changes to your data in the backend it may just be easier to use a "fetch" state trigger to just refetch ("get") your data after each backend update. Anytime you make a call to update data in your DB, upon success trigger the fetch/refetch via a useEffect hook.
const [data, setData] = React.useState();
const [fetchData, setFetchData] = useState(true);
const triggerDataFetch = () => setFetchData(t => !t);
useEffect(() => {
Axios.getValues().then((result) => {
setData(result.data);
});
}, [fetchData]);
const deleteHandler = id => async () => {
try {
await Axios.deleteValues(id); // no error, assume success
triggerDataFetch(); // <-- trigger refetch
} catch(err) {
// whatever you want to do with error
}
};
I think you wrong in here :
useEffect(() => {
Axios.getValues().then((result) => {
setData(result.data); // result has no data property
});
}, [data]);
Please try change to this
useEffect(() => {
Axios.getValues().then((result) => {
console.log("RESULT",result); // check the actually response from API
setData(result.results); // the response of data is called results
});
}, [data]);
import axios from "axios"; const getValues = async () => { const values = await axios .get("https://pokeapi.co/api/v2/type/")
.then((response) => { return response.data; }) .catch(function (error)
{ console.log(error); }); return values; };
I don't know why but what are you trying to achieve with this. You should either use async/await clause or then clause but you are using both atleast have some good practice of coding first.
Second I think you should use async await inside try catch and remove then/catch phrases to make your code more understandable, then if you store your result inside values then simply return values.data and your problem might be resolved.
Since the deleteValues function deletes a specific object from the array on the server-side, I have decided to filter the list of objects in the array in order to remove the matching id to reflect on the front end. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter
This is how I approached it.
{data.map((values) => {
...
<Button
onClick={() => {
setData(data.filter((item) => item.id !== values.id)); // <- Filter
Axios.deleteValues(values.id)
.then(data => setData(data));
}}
/>
...
})};

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)
});
});
}

Rendering component before fetching data has been finished

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++;
}
};

How to display item name after axios request

I try to display the item name (here the item is an ingredient) after getting it by an axios request. I don't understand what I need to do to use to "return" the item name.
Axios return the name of the item without any problem so I tried to display it with return <p>{response.data.name}</p> but nothing is displayed.
I juste have this message : "Expected to return a value in arrow function"
ListIng is called (props.recipe.ing_list = ["whateverid", "whateverid"]) :
<td><ListIng list={props.recipe.ing_list} /></td>
and I try this to display the name of the item :
const ListIng = props => (
props.list.map((item) => {
axios.get('http://localhost:4000/ingredient/' + item)
.then(response => {
return <p>{response.data.name}</p>
})
.catch(function (error) {
console.log(error)
})
})
)
It's my first post so if there is anything I can improve, don't hesitate to tell me ;-)
You are returning value from .then callback function. Returned value will be passed to nest .then if any, but will not be used as return value from functional component.
As you're using async call, you should use state, to make React know, that data is ready and component should be re-rendered. You can use React Hooks to achieve this like below (not tested, use as hint)
const ListIng = props => {
const [data, setData] = useState([]); // Initial data will be empty array
props.list.map((item) => {
axios.get('http://localhost:4000/ingredient/' + item)
.then(response => {
setData(e => ([...e, response.data.name])); // On each response - populate array with new data
})
.catch(function (error) {
console.log(error)
})
})
// Display resulting array as data comes
return <div>{data.map(d => ({<p>{d}</p>}))}</div>
}
Usually api calls should be inside componentDidMount() lifecycle method.
import React,{Component} from 'react';
const ListIng extends Component {
state={name:null};
componentDidMount() {
this.props.list.map((item) => {
axios.get('http://localhost:4000/ingredient/' + item)
.then(response => {
this.setState({name:response.data.name});
})
.catch(function (error) {
console.log(error)
})
})
}
render() {
return(
<p>{this.state.name}</p>
);
}
};

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.

Categories

Resources