I'm trying to learn React and I'm a beginner when it comes to Javascript. Right now I'm working on an app that is fetching data from Flickr's API. The problem is that when I try to use the map method on the props in the Main.js component I get an error saying "Uncaught TypeError: this.props.photos.map is not a function". After searching here on Stackoverflow I think the problem is that this.props are javascript objects and not an array. The problem is that I can't figure out how to make it an array. Can anyone explain what I'm doing wrong?
My code:
class App extends Component {
constructor() {
super();
this.state = {
}
}
componentDidMount() {
let apiKey = 'xxxxxxxxxxxxxxxxxx';
let searchKeyword = 'nature';
let url = `https://api.flickr.com/services/
rest/?api_key=${apiKey}&method=flickr.photos.
search&format=json&nojsoncallback=1&&per_page=50
&page=1&text=${searchKeyword}`;
fetch(url)
.then(response => response.json())
.then(data => data.photos.photo.map((x) => {
this.setState({
farm: x.farm,
id: x.id,
secret: x.secret,
server: x.server})
// console.log(this.state)
}))
}
render() {
return (
<div className="App">
<Header />
<Main img={this.state.photos} />
<Navigation />
</div>
);
}
}
export default class Main extends Component {
render() {
return(
<main className="main">
{console.log(this.props.photos)}
</main>
)
}
}
Edit:
Why is this.props.img undefined first?
Screen shot from console.log(this.props.img)
fetch(url)
.then(response => response.json())
.then(data => data.photos.photo.map((x) => {
this.setState({
farm: x.farm,
id: x.id,
secret: x.secret,
server: x.server})
}))
What is happening is that your map function in your promise is resetting the component's state for every photo that is returned. So your state will always be the last object in your list of returned photos.
Here is a more simplified example of what I am referring to
const testArray = [1,2,3,4];
let currentState;
testArray.map((value) => currentState = value)
console.log(currentState);
What you want to do is this
const testArray = [1,2,3,4];
let currentState;
//Notice we are using the return value of the map function itself.
currentState = testArray.map((value) => value)
console.log(currentState);
For what you are trying to accomplish, you want your state to be the result of the map function (since that returns an array of your results from the map). Something like this:
fetch(url)
.then(response => response.json())
.then(data =>
this.setState({
photos:
data.photos.photo.map((x) => ({
farm: x.farm,
id: x.id,
secret: x.secret,
server: x.server
}))
})
)
This error might also happen if you try to provide something else other than the array that .map() is expecting, even if you declare the variable type properly. A hook-based example:
const [myTwinkies, setMyTwinkies] = useState<Twinkies[]>([]);
useEffect(() => {
// add a twinky if none are left in 7eleven
// setMyTwinkies(twinkiesAt711 ?? {}); // ---> CAUSES ".map is not a function"
setMyTwinkies(twinkiesAt711 ?? [{}]);
}, [twinkiesAt711, setMyTwinkies]);
return (<ul>
{myTwinkies.map((twinky, i)=> (
<li key={i}>Twinky {i}: {twinky?.expiryDate}</li>
))}
</ul>)
Just check the length of the array before going for the map. If the len is more than 0 then go for it, otherwise, ignore it.
data.photos.photo.map.length>0 && data.photos.photo.map(........)
Related
I have an array data in my state, that contains a collection of objects, however when I try to access and render some of the information stored in the objects, none of the expected text appears on the page.
My current code for this is in my render function as follows:
<ul>
{this.state.data.map(match =>
<Link to={'teams/'+ team.homeId} key={match.id}><li>{match.homeTeam}</li></Link>
<ul>
When I check the elements in my console, I see undefined where I'm expecting my data.
The information is retrieved inside an await axios.get() request inside my async componentDidMount() function, which leads me to believe that the data may not be displaying correctly as it has not yet been retrieved when the render occurs, however I'm not sure.
I've previously tried to define let match = this.state.data || {} at the beginning of my render, so that my render would reload once the setState had taken place in my componentDidMount, however this did not work. Any directions would be appreciated.
EDIT as per MonteCristo request;
class Welcome extends Component {
constructor(props) {
super(props)
this.state = {
data: []
}
}
async componentDidMount() {
let res = await axios.get(`https://soccer.sportmonks.com/api/v2.0/livescores?api_token=${API}`)
let matchInfo = new Array(res.data.data.length).fill().map(_ => ({}))
res.data.data.forEach((id, i) => Object.assign(matchInfo[i], {id: id.id, homeScore: id.scores.localteam_score}, {awayScore: id.scores.visitorteam_score}))
res.data.data.forEach((id, i) => {
axios.get(`https://soccer.sportmonks.com/api/v2.0/teams/${id.localteam_id}?api_token=${API}`)
.then(res1 => {
Object.assign(matchInfo[i], {homeId: res1.data.data.id, homeTeam: res1.data.data.name})
})
})
res.data.data.forEach((id, i) => {
axios.get(`https://soccer.sportmonks.com/api/v2.0/teams/${id.visitorteam_id}?api_token=${API}`)
.then(res2 => {
Object.assign(matchInfo[i], {awayId: res2.data.data.id, awayTeam: res2.data.data.name})
})
})
res.data.data.forEach((id, i) => {axios.get(`https://soccer.sportmonks.com/api/v2.0/leagues/${id.league_id}?api_token=${API}`)
.then(res3 => {
Object.assign(matchInfo[i], {leagueName: res3.data.data.name})
})
})
this.setState({
data: [...this.state.data, ...matchInfo]
})
}
render() {
return (
<div>
<p className="text-xl font-semibold">Live Matches</p>
<div>
<ul>
{this.state.data.map(match =>
<Link to={'teams/'+ match.homeId} key={match.id}><li>{match.homeTeam}</li></Link>)}
</ul>
</div>
</div>
);
}
}
export default Welcome;
You need to actually wait for the requests to complete.
You're doing a this.setState({ data: [...this.state.data, ...matchInfo] }) at the bottom, but you are mutating the matchInfo objects asynchronously, so setState is occurring before all those axios requests complete. Mutating state will not cause re-renders, only setting it via setState.
If you await all your axios calls instead and build your object before calling setState, you'll get what you are looking for:
async componentDidMount() {
const soccerApi = axios.create({
baseURL: 'https://soccer.sportmonks.com/api/v2.0',
params: { api_token: API }
});
const res = await soccerApi.get(`/livescores`);
const matchInfo = await Promise.all(res.data.data.map(async row => {
const [homeTeamInfo, awayTeamInfo, leagueInfo] = await Promise.all([
soccerApi.get(`/teams/${row.localteam_id}`),
soccerApi.get(`/teams/${row.visitorteam_id}`),
soccerApi.get(`/leagues/${row.league_id}`)
]);
return {
id: row.id,
homeScore: row.scores.localteam_score,
awayScore: row.scores.visitorteam_score,
homeId: homeTeamInfo.data.data.id,
homeTeam: homeTeamInfo.data.data.name,
awayId: awayTeamInfo.data.data.id,
awayTeam: awayTeamInfo.data.data.name,
leagueName: leagueInfo.data.data.name
};
}));
this.setState({
data: [...this.state.data, ...matchInfo]
});
}
You need to check that data was retrieved successfully like this:
<ul>
{this.state.data && this.state.data.map(match => {
return (<Link to={'teams/'+ team.homeId} key={match.id}>{match.homeTeam}</Link>)
}
<ul>
Main issue is your axios calls in forEach. you're not waiting for them. you are calling them within the forEach instead of waiting.
so you'd need to do something like this
const allPromises = res.data.data.map((id, i) => { return axios.get() }
// then
const allResponseValues = await axios.all(allPromises).catch(console.error)
// do something with allResponses and update matchInfo
// finally
this.setState({...})
also update your render function to below for readability and clarity
{this.state.data.map(match => (
<Link to={"teams/" + match.homeId} key={match.id}>
<li>{match.homeTeam}</li>
</Link>
))}
I'm learning React Framework. When i try to fetch from my api using setState .map function dont work. It render "data.map is not a function"
This is the only Method i know:
constructor(props) {
super(props);
this.state = {
name: "secret",
email: "secret",
datas: []
};
}
componentDidMount() {
const { name, password} = this.state;
fetch(
`http://localhost:3000/api/user/account?username=${username}&email=${email}`
)
.then(res => res.json())
.then(getdata => {
this.setState({ datas: getdata });
})
.catch(err => console.log(err));
}
render() {
const { datas } = this.state;
const repoItems = datas.map(data => (
<div key={data.id}>
<p>{data.name}</p>
<p>{data.email}</p>
</div>
));
return (
<div>
{datas}
</div>
);
}
Did I wrote my code wrong or is there any other method beside from this
From the top view everything seems fine with your code. map will only run on arrays and I can see that you are setting default state to array as well. However the problem may lies when you attempt to set the state after getting response from fetch call
fetch(
`http://localhost:3000/api/user/account?username=${username}&email=${email}`
)
.then(res => res.json())
.then(getdata => {
this.setState({ datas: getdata });
})
.catch(err => console.log(err));
Here you need to make sure that whatever you are setting data, it should be an array. So in case you are not getting the array in the response set the value which is actually an array and in case the response is empty set an empty array in state instead, something like below
// may be array is a property of response
this.setState({ datas: getdata.array });
// may be getdata is empty
this.setState({ datas: getdata || [] });
I have a big problem with the ComponentWillReceiveProps.
What happens?
I have the three components, that i pass the props from component 1 to component 2 and of the component 2 i pass to component 3, where is really used..
Basically is: I pass the props of the component 1 to use in the component 3
In the component 3, i make an ajax call with the news props using ComponentWillReceiveProps. But when i pass the props it makes multiples ajax instead just one..
I dont know why.. Can someone help me?
Component 1:
render() {
return (
<Router>
<Route exact path = "/"
render = {
(props) =>
<Overview
{ ...props}
domainStatusFiltered = {
this.state.domainStatusFiltered
}
subdomainStatusFiltered = {
this.state.subdomainStatusFiltered
}
managerStatusFiltered = {
this.state.managerStatusFiltered
}
countryStatusFiltered = {
this.state.countryStatusFiltered
}
cityStatusFiltered = {
this.state.cityStatusFiltered
}
squadNameStatusFiltered = {
this.state.squadNameStatusFiltered
}
/>} / >
)
}
Component 2:
render(){
return (
<TwoColumnGrid>
<Module className="mt-2" title="Team Status">
<TeamStatus
domainStatusFiltered={this.props.domainStatusFiltered}
subdomainStatusFiltered={this.props.subdomainStatusFiltered}
managerStatusFiltered={this.props.managerStatusFiltered}
countryStatusFiltered={this.props.countryStatusFiltered}
cityStatusFiltered={this.props.cityStatusFiltered}
squadNameStatusFiltered={this.props.squadNameStatusFiltered} />
</Module>
</TwoColumnGrid>
)
}
In the Component 3, i do a verify of the user logged and make the ajax according to your level:
componentWillReceiveProps(props) {
const firstName = localStorage.getItem('nameLoggedUser');
const lastName = localStorage.getItem('lastNameLoggedUser');
const fullName = `${firstName} ${lastName}`.toLowerCase();
const loggedUserIs = localStorage.getItem("user-role");
if (loggedUserIs === 'full') {
axios.get(`/api/squadsPeopleAll/${props.managerStatusFiltered}/${props.cityStatusFiltered}/${props.countryStatusFiltered}/${props.squadNameStatusFiltered}/${props.domainStatusFiltered}/${props.subdomainStatusFiltered}`)
.then(res => {
const getIds = res.data.map(i => i.id);
const people = Object.keys(getIds).length;
const getSquad = res.data.map(i => i.squad_name);
const unicSquads = Array.from(new Set(getSquad));
const squads = Object.keys(unicSquads).length;
this.setState({
people,
squads
})
})
.catch(err => console.log(err))
axios.get(`/api/wfmskills/${props.managerStatusFiltered}/${props.cityStatusFiltered}/${props.countryStatusFiltered}/${props.squadStatusFiltered}/${props.domainStatusFiltered}/${props.subdomainStatusFiltered}`)
.then(res => {
this.setState({
totalSkills: res.data.count,
loading: false
})
})
axios.get(`/api/notupdated/${props.managerStatusFiltered}/${props.cityStatusFiltered}/${props.countryStatusFiltered}/${props.squadStatusFiltered}/${props.domainStatusFiltered}/${props.subdomainStatusFiltered}`)
.then(res => {
this.setState({
totalNotUpdated: res.data.count
})
})
axios.get(`/api/updated2017/${props.managerStatusFiltered}/${props.cityStatusFiltered}/${props.countryStatusFiltered}/${props.squadStatusFiltered}/${props.domainStatusFiltered}/${props.subdomainStatusFiltered}`)
.then(res => {
this.setState({
updated2017: res.data.count
})
})
axios.get(`/api/updated2016/${props.managerStatusFiltered}/${props.cityStatusFiltered}/${props.countryStatusFiltered}/${props.squadStatusFiltered}/${props.domainStatusFiltered}/${props.subdomainStatusFiltered}`)
.then(res => {
this.setState({
updated2016: res.data.count
})
})
axios.get(`/api/updated2018/${props.managerStatusFiltered}/${props.cityStatusFiltered}/${props.countryStatusFiltered}/${props.squadStatusFiltered}/${props.domainStatusFiltered}/${props.subdomainStatusFiltered}`)
.then(res => {
this.setState({
updated2018: res.data.count
});
})
}
if (loggedUserIs === 'L4') {
axios.get(`/api/squadsPeopleManager/${fullName}/${this.state.cityStatusFiltered}/${this.state.countryStatusFiltered}/${this.state.squadNameStatusFiltered}/${this.state.domainStatusFiltered}/${this.state.subdomainStatusFiltered}`)
.then(res => {
console.log(res.data)
const getIds = res.data.map(i => i.id);
const people = Object.keys(getIds).length;
const getSquad = res.data.map(i => i.squad_name);
const unicSquads = Array.from(new Set(getSquad));
const squads = Object.keys(unicSquads).length;
this.setState({
people,
squads
})
})
.catch(err => console.log(err))
axios.get(`/api/wfmskills/manager/${fullName}`)
.then(res => {
this.setState({
totalSkills: res.data.count
})
})
axios.get(`/api/notupdated/manager/${fullName}/${props.cityStatusFiltered}/${props.countryStatusFiltered}/${props.squadStatusFiltered}/${props.domainStatusFiltered}/${props.subdomainStatusFiltered}`)
.then(res => {
this.setState({
totalNotUpdated: res.data.count
})
})
axios.get(`/api/updated2017/manager/${fullName}/${props.cityStatusFiltered}/${props.countryStatusFiltered}/${props.squadStatusFiltered}/${props.domainStatusFiltered}/${props.subdomainStatusFiltered}`)
.then(res => {
this.setState({
updated2017: res.data.count
})
})
axios.get(`/api/updated2016/manager/${fullName}/${props.cityStatusFiltered}/${props.countryStatusFiltered}/${props.squadStatusFiltered}/${props.domainStatusFiltered}/${props.subdomainStatusFiltered}`)
.then(res => {
this.setState({
updated2016: res.data.count
})
})
axios.get(`/api/updated2018/manager/${fullName}/${props.cityStatusFiltered}/${props.countryStatusFiltered}/${props.squadStatusFiltered}/${props.domainStatusFiltered}/${props.subdomainStatusFiltered}`)
.then(res => {
this.setState({
updated2018: res.data.count
}, () => this.setState({
loading: false
}));
})
}
}
The problem is, for example: If I give the setState only in subdomainStatusFiltered it from multiple ajax calls and returns the result several times, I did a test and put a console.log ('test') it is rendered several times, I do not know why, or I'm using the wrong method and I do not know, look that it rerender several times... I just want to call a single time when I change some state, can someone help me, please?
You can use componentDidMount()
componentDidMount() {
// here you can use your API calls with this.props
}
If still you want use componentWillReceiveProps then you can do as below:
componentWillReceiveProps(nextProps) {
if(!this.props.managerStatusFiltered && nextProps.managerStatusFiltered){
// now you can use nextProps.managerStatusFiltered
}
}
First, have you compare props with nextProps to make sure props was different? componentWillReceiveProps does not guarantee the props was different. Instead, it'll be invoked everytime your component needs to re-render.
And secondly, I think the best way to do it is moving the async calls to the top level component and passing the response data to children that need it. This way you can avoid complex state check.
If you want events to occur once and only once over the lifetime of a React component, you should use componentDidMount.
Please note that componentWillReceiveProps is a legacy lifecycle method and should not be used anymore in new projects. This method will soon only be available with the UNSAFE_ prefix when React 17 will be released. From the react documentation:
UNSAFE_componentWillReceiveProps(nextProps)
Note:
Using this lifecycle method often leads to bugs and inconsistencies,
and for that reason it is going to be deprecated in the future.
If you need to perform a side effect (for example, data fetching or an
animation) in response to a change in props, use componentDidUpdate
lifecycle instead.
I'm making an application where I have to grab certain data from the Github API. I need to grab the name, url, language and latest tag. Because the latest tag is in a separate url, I need to make another fetch call there to grab that data.
I'm running into a certain amount of errors.
1st being the typeError cannot read property 'name' of undefined. I'm sure this is from the fetch call to the tag url where there isn't any data. I'm not really sure how to check if it's undefined. I've tried calling checking to see if the typeof data is undefined and so on but still get the error.
2nd problem being my tag url data doesn't show up with the other data. I'm sure I'm chaining the data wrong because when I click the add button it shows up.
Here is my code:
import React, { Component } from 'react'
import './App.css'
class App extends Component {
state = {
searchTerm: '',
repos: [],
favourites: []
}
handleChange = e => {
const { searchTerm } = this.state
this.setState({ searchTerm: e.target.value })
if (searchTerm.split('').length - 1 === 0) {
this.setState({ repos: [] })
}
}
findRepos = () => {
const { searchTerm } = this.state
// First api call here
fetch(`https://api.github.com/search/repositories?q=${searchTerm}&per_page=10&access_token=${process.env.REACT_APP_TOKEN}
`)
.then(res => res.json())
.then(data => {
const repos = data.items.map(item => {
const { id, full_name, html_url, language } = item
const obj = {
id,
full_name,
html_url,
language,
isFavourite: false
}
// Second api call here. I need the data from map to get the tags for the correct repo
fetch(`https://api.github.com/repos/${full_name}/tags`)
.then(res => res.json())
.then(data => {
obj.latest_tag = data[0].name
})
.catch(err => console.log(err))
return obj
})
this.setState({ repos })
})
.catch(err => console.log(err))
}
render() {
const { searchTerm, repos, favourites } = this.state
return (
<div className="App">
<h1>My Github Favorites</h1>
<input
type="text"
placeholder="search for a repo..."
value={searchTerm}
onChange={e => this.handleChange(e)}
onKeyPress={e => e.key === 'Enter' && this.findRepos()}
/>
<button
type="submit"
onClick={this.findRepos}>
Search
</button>
<div className="category-container">
<div className="labels">
<h5>Name</h5>
<h5>Language</h5>
<h5>Latest Tag</h5>
</div>
// Here I list the data
{repos.map(repo => (
<div key={repo.id}>
<a href={repo.html_url}>{repo.full_name}</a>
<p>{repo.language}</p>
{repo.latest_tag ? <p>{repo.latest_tag}</p> : <p>-</p>}
<button onClick={() => this.addToFavs(repo)}>Add</button>
</div>
))}
<h1>Favourites</h1>
{favourites.map(repo => (
<div key={repo.id}>
<a href={repo.html_url}>{repo.full_name}</a>
<p>{repo.language}</p>
<p>{repo.latest_tag}</p>
<button>Remove</button>
</div>
))}
</div>
</div>
)
}
}
export default App
If you use Promise.all(), you could rewrite your code like the following.
findRepos = () => {
const { searchTerm } = this.state;
// First api call here
const first = fetch(
`https://api.github.com/search/repositories?q=${searchTerm}&per_page=10&access_token=${
process.env.REACT_APP_TOKEN
}`
);
// Second api call here. I need the data from map to get the tags for the correct repo
const second = fetch(`https://api.github.com/repos/${full_name}/tags`);
Promise.all([first, second])
.then((res) => Promise.all(res.map(r => r.json())))
.then([data1, data2] => {
data1.then((firstData) => {
/*Do something you want for first.*/
});
data2.then((secondData) => {
/*Do something you want for second.*/
});
})
.catch((err) => console.log(err));
};
Hope this works for you.
Trying to render a list of movies from a random API and eventually filter them.
componentDidMount() {
var myRequest = new Request(website);
let movies = [];
fetch(myRequest)
.then(function(response) { return response.json(); })
.then(function(data) {
data.forEach(movie =>{
movies.push(movie.title);
})
});
this.setState({movies: movies});
}
render() {
console.log(this.state.movies);
console.log(this.state.movies.length);
return (
<h1>Movie List</h1>
)
}
If I render this I can only print my state and not access what is inside.
How would I create a list of LIs and render a UL?
Thanks
A few things. fetch is asynchronous, so you're essentially just going to be setting movies to an empty array as this is written. If data is an array of movies, you can just set that directly in your state rather than copying it to a new array first. Finally, using an arrow function for the final callback in the promise will allow you to use this.setState without having to explicitly bind the function.
Finally, you can use JSX curly brace syntax to map over the movies in your state object, and render them as items in a list.
class MyComponent extends React.Component {
constructor() {
super()
this.state = { movies: [] }
}
componentDidMount() {
var myRequest = new Request(website);
let movies = [];
fetch(myRequest)
.then(response => response.json())
.then(data => {
this.setState({ movies: data })
})
}
render() {
return (
<div>
<h1>Movie List</h1>
<ul>
{this.state.movies.map(movie => {
return <li key={`movie-${movie.id}`}>{movie.name}</li>
})}
</ul>
</div>
)
}
}
First of all, since fetch is asynchronous, you will most likely call setState before the fetch call has completed, and you obviously don't want to do that.
Second, if the data returned is an array of movies, do not iterate over it, just assign the array altogether to the movies state variable.
So, in short, this is what your componentDidMount method should look like :
componentDidMount() {
var myRequest = new Request(website);
fetch(myRequest)
.then(res => res.json())
.then(movies =>
this.setState({ movies }) // little bit of ES6 assignment magic
);
}
Then in your render function you can do something like:
render() {
const { movies } = this.state.movies;
return (
<ul>
{ movies.length && movies.map(m => <li key={m.title}>{m.title}</li>) }
</ul>
);
}
Gotten your movies in state you can do the next to render the list of movies:
render() {
return (
<h1>Movie List</h1>
<ul>
{
this.state.movies.map((movie) =>
<li key={movie}>movie</li>
)
}
</ul>
);
}