React .map only returning first item - arrays

I've just started my React journey recently. I am currently trying to render properties of an array of objects which is returned from my controller.
The json:
[
{
"reportID":4,
"reportDescription":"Commission Bonus Register",
"reportNotes":"",
"reportName":"CommissionBonusRegister"
},
{
"reportID":5,
"reportDescription":"Reset Government ID",
"reportNotes":"",
"reportName":"ResetGovtID"
},
{
"reportID":6,
"reportDescription":"Distributor Chase Up Report",
"reportNotes":"",
"reportName":"DistributorChaseUpReport"
},
{
"reportID":7,
"reportDescription":"Vietnam Distributor Export",
"reportNotes":"",
"reportName":"VietnamDistributorExport"
},
{
"reportID":8,
"reportDescription":"Vietnam Order Export",
"reportNotes":"",
"reportName":"VietnamOrderExport"
},
{
"reportID":9,
"reportDescription":"Distributor List by status and period",
"reportNotes":"",
"reportName":"DistributorsList"
}
]
React component code:
constructor(props) {
super(props);
this.state = {
linkscontent: [],
loading: true,
refresh: true
}
this.populateReportsLinks = this.populateReportsLinks.bind(this);
}
async componentDidMount() {
await this.populateReportsLinks();
}
render() {
let contents = this.state.loading
? <p><em>Loading...</em></p>
:
this.state.linkscontent.map(([reports], index) => {
return <li key={reports.reportID}>{reports.reportDescription}</li>
});
return (
<div>
<h1 id="tabelLabel" >Reports</h1>
<ul>
{contents}
</ul>
</div>
);
}
async populateReportsLinks() {
const response = await fetch('reports')
.then(res => res.json())
.then(data =>
this.setState({ linkscontent: [data], error: data.error || null, loading: false, refresh: !this.state.refresh }));
return response;
}
After two days of frustration I have finally managed to get the first item to display, but only the first item. Ive read so many articles and forum solutions that seem to indicate this should work. Can anyone help me figure out what is wrong here?

Remove the [data] to just this.setState({ linkcontent: data, ...restOfUpdates }) after you have fetched your data.
While mapping don't destructure with [reports] just use the reports.
async componentDidMount() {
await this.populateReportsLinks();
}
render() {
let contents = this.state.loading
? <p><em>Loading...</em></p>
: this.state.linkscontent.map((reports, index) => {
return <li key={reports.reportID}>{reports.reportDescription}</li>
});
return (
<div>
<h1 id="tabelLabel" >Reports</h1>
<ul>
{contents}
</ul>
</div>
);
}
async populateReportsLinks() {
const response = await fetch('reports')
.then(res => res.json())
.then(data =>
this.setState({ linkscontent: data, error: data.error || null, loading: false, refresh: !this.state.refresh }));
return response;
}

You have a few problems with your logic. Let's look at them one by one.
When you set up state with your data
this.setState({ linkscontent: [data],...}
So when you do the above its basically makes linkscontent an array but only of one length. That means on its first index you have an array of your data.
When you run map like this
this.state.linkscontent.map(([reports], index)
That means you want to iterate through each index of linkscontent but since you have only one index in linkscontent you will get only one item printed.
How to fix.
There are a few ways to fix it. You can try saving data into the state as per below code. This will make linkscontent an array with the data source.
this.setState({ linkscontent: [...data],...}
Or
this.setState({ linkscontent: data,...}
then run map like this
this.state.linkscontent.map((report, index) => <li key={report.reportID}>{report.reportDescription}</li>)
With your current version of setting linkscontent of one length, you can run your map like this as well
this.state.linkscontent.length && this.state.linkscontent[0].map((report, index) => ...)

Yeah it comes down to your state not the mapping function. spread the results into an array. this also happens quite often when you incorrectly mutate or update the state.

Related

React fetch doesn't display value from nested object

I have a problem with getting value from nestle object. I want to get value name from studySets (this is a nested object in object user). The program knows that it is an object but it can't display value (show two dots because, in the database, it is two user objects). I have added a screenshot with the response. Please tell me where is the problem.
WordSet.js
export class WordSet extends React.Component {
constructor(props) {
super(props);
this.state = {
users: [],
};
}
componentDidMount() {
fetch(URL)
.then((response) => response.json())
.then((json) => {
this.setState({ users: json });
});
}
render() {
const { users } = this.state;
return (
<div>
<ul>
{users.map((user) => (
<li key={user.id}>{user.studySets.name}</li>
))}
</ul>
</div>
);
}
}
export default WordSet;
ok basically you are treating the studySets as object which is rather an array. so either you have to loop over the studySets array as well or if you always want the data of the first user then do something like this as I mentioned in the comment:
{user.studySets[0].name}
in the
<li>

conditional rendering at empty response of API in react js

I am building a little react js application where users can select a few filters and get response accordingly. For some values selected by users, nothing is found from database and I want to show "nothing found" message.
I tried using if & else conditional operators which is not producing results. Below is the code.
.then(res => {
if(!res.data.length){
return(
<div>
<h1>nothing found.</h1>
</div>
)
}
else{
this.setState({ data: res.data,urlList:[] })
console.log(this.state)
}
})
Now If I do this
.then(res => {
if(!res.data.length){
console.log('nothing found')
}
else{
this.setState({ data: res.data,urlList:[] })
console.log(this.state)
}
})
I am getting a response on console. What I am doing wrong ?
declare a new state variable like
constructor(props){
super(props);
this.state = {
noData: ''
}
}
And here
.then(res => {
if(!res.data.length){
this.setState({noData: 'nothing found'});
console.log('nothing found')
}
else{
this.setState({ data: res.data,urlList:[], noData: '' })
console.log(this.state)
}
})
And in render just display this.state.noData
render(){
return(
<div>
<h1>{this.state.noData}</h1>
</div>
)
}
Log out the res.data object for a case when nothing is found. It may not be an empty array as your code assumes, but might contain some message to the effect that nothing on the database matches your query. You need to check for this rather than simply !res.data.

Lifecycle hooks - Where to set state?

I am trying to add sorting to my movie app, I had a code that was working fine but there was too much code repetition, I would like to take a different approach and keep my code DRY. Anyways, I am confused as on which method should I set the state when I make my AJAX call and update it with a click event.
This is a module to get the data that I need for my app.
export const moviesData = {
popular_movies: [],
top_movies: [],
theaters_movies: []
};
export const queries = {
popular:
"https://api.themoviedb.org/3/discover/movie?sort_by=popularity.desc&api_key=###&page=",
top_rated:
"https://api.themoviedb.org/3/movie/top_rated?api_key=###&page=",
theaters:
"https://api.themoviedb.org/3/movie/now_playing?api_key=###&page="
};
export const key = "68f7e49d39fd0c0a1dd9bd094d9a8c75";
export function getData(arr, str) {
for (let i = 1; i < 11; i++) {
moviesData[arr].push(str + i);
}
}
The stateful component:
class App extends Component {
state = {
movies = [],
sortMovies: "popular_movies",
query: queries.popular,
sortValue: "Popularity"
}
}
// Here I am making the http request, documentation says
// this is a good place to load data from an end point
async componentDidMount() {
const { sortMovies, query } = this.state;
getData(sortMovies, query);
const data = await Promise.all(
moviesData[sortMovies].map(async movie => await axios.get(movie))
);
const movies = [].concat.apply([], data.map(movie => movie.data.results));
this.setState({ movies });
}
In my app I have a dropdown menu where you can sort movies by popularity, rating, etc. I have a method that when I select one of the options from the dropwdown, I update some of the states properties:
handleSortValue = value => {
let { sortMovies, query } = this.state;
if (value === "Top Rated") {
sortMovies = "top_movies";
query = queries.top_rated;
} else if (value === "Now Playing") {
sortMovies = "theaters_movies";
query = queries.theaters;
} else {
sortMovies = "popular_movies";
query = queries.popular;
}
this.setState({ sortMovies, query, sortValue: value });
};
Now, this method works and it is changing the properties in the state, but my components are not re-rendering. I still see the movies sorted by popularity since that is the original setup in the state (sortMovies), nothing is updating.
I know this is happening because I set the state of movies in the componentDidMount method, but I need data to be Initialized by default, so I don't know where else I should do this if not in this method.
I hope that I made myself clear of what I am trying to do here, if not please ask, I'm stuck here and any help is greatly appreciated. Thanks in advance.
The best lifecycle method for fetching data is componentDidMount(). According to React docs:
Where in the component lifecycle should I make an AJAX call?
You should populate data with AJAX calls in the componentDidMount() lifecycle method. This is so you can use setState() to update your component when the data is retrieved.
Example code from the docs:
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
error: null,
isLoaded: false,
items: []
};
}
componentDidMount() {
fetch("https://api.example.com/items")
.then(res => res.json())
.then(
(result) => {
this.setState({
isLoaded: true,
items: result.items
});
},
// Note: it's important to handle errors here
// instead of a catch() block so that we don't swallow
// exceptions from actual bugs in components.
(error) => {
this.setState({
isLoaded: true,
error
});
}
)
}
render() {
const { error, isLoaded, items } = this.state;
if (error) {
return <div>Error: {error.message}</div>;
} else if (!isLoaded) {
return <div>Loading...</div>;
} else {
return (
<ul>
{items.map(item => (
<li key={item.name}>
{item.name} {item.price}
</li>
))}
</ul>
);
}
}
}
Bonus: setState() inside componentDidMount() is considered an anti-pattern. Only use this pattern when fetching data/measuring DOM nodes.
Further reading:
HashNode discussion
StackOverflow question

React trying to use .map to populate data from multiple APIs into the same mapped container

I'm trying to practice integrating data from separate APIs, but I'm having trouble conceptualizing what the right way is to do so.
Basically, I have two sample APIs I'm fetching from:
1. const API = 'https://hn.algolia.com/api/v1/search?query='; (this.state.hits)
const DEFAULT_QUERY = 'redux';
and
2. https://randomuser.me/api/?results=50 (this.state.randomPeople)
I pull them into their own arrays in state via a method called in componentDid Mount.
this.state = {
hits: [],
randomPeople: [],
};
Ideally, I'd like to map over both of them and have data available from each .map result to populate in a single container, something like:
<div>
<img src={random.thumbnailPic}/>
<h3>{random.name.first}</h3>
<h3>{random.name.last}</h3>
<p>{hit.title}</p>
</div>
Just not sure how to approach this the best way. I have only mapped over one data source when populating the results to a container. Should I combine the two arrays and store them together in state? I looked at Lodash, would that work here? Or is there a better way to accomplish this that I just haven't found?
Right now I just have them right on top of another in render() :
{hits.map(hit => (
<div key={hit.objectID}>
<a href={hit.url}>{hit.title}</a>
</div>
))}
{randomPeople.map(rando => (
<div key={random.email}>
<img src={random.picture.medium} />
<h3>Author: {random.name.first} {random.name.last}</h3>
</div>
))}
And here are my methods:
fetchHits = () => {
fetch(API + DEFAULT_QUERY)
.then(response => {
if (response.ok) {
return response.json();
} else {
throw new Error('Something went wrong ... ');
}
})
.then(data => this.setState({ hits: data.hits, isLoading: false }))
.catch(error => this.setState({ error, isLoading: false }));
};
fetchRandomPeople = () => {
fetch('https://randomuser.me/api/?results=50')
.then(results => results.json())
.then(data => this.setState({ randomPeople: data.results }));
};
componentDidMount() {
this.fetchRandomPeople();
this.fetchHits();
this.setState({ isLoading: true });
}
Thanks!
If you're assuming that hits and randompeople are going to be the same length, or if you can somehow align the two arrays, you could add the index parameter to your .map() function:
{randomPeople.map((rando, i) => (
<div key={rando.email}>
<img src={rando.thumbnailPic}/>
<h3>{rando.name.first}</h3>
<h3>{rando.name.last}</h3>
<p>{hits[i].title}</p>
</div>
)}

React component issues with state, setState and render

I am building a React based project for study purposes. I am stuck on making a table component, that renders itself and then sends ajax request to mbaas backend to get all book entries and fill each on a new row. Here is what I've come up so far. Please forgive the large chunk of code, but since I don't yet fully understand the interactions between methods, state and render() here is the whole class:
class BooksTable extends Component {
constructor(props) {
super(props);
this.state = {
books: []
};
this.updateState = this.updateState.bind(this);
this.initBooks = this.initBooks.bind(this);
}
componentDidMount() {
let method = `GET`;
let url = consts.serviceUrl + `/appdata/${consts.appKey}/${consts.collection}`;
let headers = {
"Authorization": `Kinvey ${sessionStorage.getItem(`authToken`)}`,
"Content-Type": `application/json`
};
let request = {method, url, headers};
$.ajax(request)
.then(this.initBooks)
.catch(() => renderError(`Unable to connect. Try again later.`));
}
deleteBook(id) {
let method = `DELETE`;
let url = consts.serviceUrl + `/appdata/${consts.appKey}/${consts.collection}/${id}`;
let headers = {
"Authorization": `Kinvey ${sessionStorage.getItem(`authToken`)}`,
"Content-Type": `application/json`
};
let request = {method, url, headers};
$.ajax(request)
.then(() => this.updateState(id))
.catch(() => renderError(`Unable to delete, something went wrong.`));
}
updateState(id) {
for (let entry of this.state.books.length) {
if (entry.id === id) {
// Pretty sure this will not work, but that is what I've figured out so far.
this.state.books.splice(entry);
}
}
}
initBooks(response) {
console.log(`#1:${this.state.books});
console.log(`#2:${this});
for (let entry of response) {
this.setState({
books: this.state.books.concat([{
id: entry._id,
name: entry.name,
author: entry.author,
description: entry.description,
price: Number(entry.name),
publisher: entry.publisher
}])
}, () => {
console.log(`#3${this.state.books}`);
console.log(`#4${this}`);
});
}
}
render() {
return (
<div id="content">
<h2>Books</h2>
<table id="books-list">
<tbody>
<tr>
<th>Title</th>
<th>Author</th>
<th>Description</th>
<th>Actions</th>
</tr>
{this.state.books.map(x =>
<BookRow
key={x.id}
name={x.name}
author={x.author}
description={x.description}
price={x.price}
publisher={x.publisher} />)}
</tbody>
</table>
</div>
);
}
}
Now the BookRow is not very interesting, only the onClick part is relevant. It looks like this:
<a href="#" onClick={() => this.deleteBook(this.props.id)}>{owner? `[Delete]` : ``}</a>
The Link should not be visible if the logged in user is not publisher of the book. onClick calls deleteBook(id) which is method from BookTable. On successful ajax it should remove the book from state.books (array) and render.
I am particularly confused about the initBooks method. I've added logs before the loop that populates the state and as callbacks for when the state is updated. Results from log#1 and log#3 are identical, same for logs#2#4. Also if I expand log#2 (before setState) or log#4, both of those show state = [1]. How does this make sense? Furthermore if you take a look at logs#1#3 - they print [ ]. I am probably missing some internal component interaction, but I really cant figure out what.
Thanks.
The setState doesn't immediately update the state. So in the second iteration of your for loop, you wont be getting the new state. So make your new book list first and then set it once the new list is prepared.
Try this:
initBooks(response) {
console.log(this.state.books, "new books not set yet")
let newBooks = []
for (let entry of response) {
newBooks.push({
id: entry._id,
name: entry.name,
author: entry.author,
description: entry.description,
price: Number(entry.name),
publisher: entry.publisher
})
}
this.setState({books: [...this.state.books, newBooks]}, () => {
console.log(this.state.books, "new books set in the state")
})
}
try this:
initBooks(response = {}) {
const books = Object.keys(response);
if (books.length > 0) {
this.setState((prevState) => {
const newBooks = books.reduce((acc, key) => {
const entry = response[key];
return [ ...acc, {
id: entry._id,
name: entry.name,
author: entry.author,
description: entry.description,
price: Number(entry.name),
publisher: entry.publisher
}];
}, prevState.books);
return { books: newBooks };
});
}
}
What I did here?
setState only if needed (ie only if there is data from API response)
avoids state mutation, no setState inside loop
using a function ((prevState, props) => newState) to ensure atomic
update for reliability.
Points to ponder:
Don't mutate your state
Avoid setState inside a loop; instead prepare the new state object and do a one-time setState)
If you want to access the previous state while calling setState, it is advised to do it like this:
this.setState((prevState, props) => {
return { counter: prevState.counter + props.increment };
});
setState() does not immediately mutate this.state but creates a
pending state transition. Accessing this.state after calling this
method can potentially return the existing value.

Resources