inside of my componentDidMount method I'm trying to make array of objects by using fetch. In my head it looks like this - on state I keep variable "loading" (true by default), and when my method is done with fetching it set it to false. On render method I've put if statement. But in real life my method filled array doesn't get executed (first console.log gets executed, second is not), . I'm losing my mind with this.
import { Company } from "../company/company.component";
export class CompanyList extends Component {
constructor(props) {
super(props);
this.state = {
tempResult: 10,
newArray: [],
loading: true,
};
}
componentDidMount = () => {
console.log(this.state.loading,"1");
const filledArray = this.props.companies.map((item) => {
fetch(`xxxx/incomes/${item.id}`)
.then((response) => response.json())
.then((data) => {
let transactionsToFloat = data.incomes.map((item) =>
parseFloat(item.value)
);
let result = transactionsToFloat.reduce((acc, num) => {
return acc + num;
}, 0);
result = Math.round(result * 100) / 100;
this.setState({ tempResult: result, loading: false });
console.log(item.id, item.name, item.city, result);
return {
id: item.id,
name: item.name,
city: item.city,
totalIncome: result,
};
});
this.setState({ loading: false });
return true;
});
this.setState({ newArray: filledArray });
};
render() {
if (this.state.loading) {
return <h1>Loading...</h1>;
} else if (!this.state.loading) {
return (
<div>
{/* <button onClick={this.handleClick}>Button</button> */}
<table>
<thead>
<tr>
<th> Id </th>
<th> Name </th>
<th> City </th>
<th> Total income </th>
</tr>
</thead>
{this.state.newArray.map((item) => (
<Company key={item.id} company={item} />
))}
</table>
</div>
);
}
}
}
Cheers
fetch is async, when you do this.setState({ loading: false }) after fetch, this line of code will be executed right away, before promise is even resolved. you are also not returning the data, but true values instead.
given that you are executing an array of promises, you may consider return fetch promises and wrap your array of promises with a Promise.all:
Promise.all(this.props.companies.map((item) => { return fetch().then().then() })
.then(results => this.setState({ newArray: results, loading: false }))
.catch(error => ... handle error here)
there is a caveat that Promise.all will reject if one of the promises fails. if you dont want that behavior you could use Promise.allSettled instead. allSettled will never reject and it returns instead an array of objects, with status and value keys.
Related
Just wondering how can I go about, rendering the total length of the this.state.apiData array on the output screen. so I can go on and use conditional formatting with the outputted results.
that the user can see the total number of stock objects returned. T
import React, { Component } from "react";
class App extends Component {
constructor(props) {
super(props);
// create three state variables.
// apiData is an array to hold our JSON data
// isFetched indicates if the API call has finished
// errorMsg is either null (none) or there is some error
this.state = {
apiData: [],
isFetched: false,
errorMsg: null
};
}
// componentDidMount() is invoked immediately after a
// component is mounted (inserted into the tree)
async componentDidMount() {
try {
const API_URL =
"#";
// Fetch or access the service at the API_URL address
const response = await fetch(API_URL);
// wait for the response. When it arrives, store the JSON version
// of the response in this variable.
const jsonResult = await response.json();
// update the state variables correctly.
this.setState({ apiData: jsonResult.stockData });
this.setState({ isFetched: true });
} catch (error) {
// In the case of an error ...
this.setState({ isFetched: false });
// This will be used to display error message.
this.setState({ errorMsg: error });
} // end of try catch
} // end of componentDidMount()
// Remember our three state variables.
// PAY ATTENTION to the JSON returned. We need to be able to
// access specific properties from the JSON returned.
// Notice that this time we have three possible returns for our
// render. This is conditional rendering based on some conditions
render() {
if (this.state.errorMsg) {
return (
<div className="error">
<h1>We're very sorry: An error has occured in the API call</h1>
<p>The error message is: {this.state.errorMsg.toString()}</p>
</div>
); // end of return.
} else if (this.state.isFetched === false) {
return (
<div className="fetching">
<h1>We are loading your API request........</h1>
<p>Your data will be here very soon....</p>
</div>
); // end of return
} else {
// we have no errors and we have data
return (
<div className="App">
<div className="StocksTable">
<h1>CS385 - Stocks API Display</h1>
<table border="1">
<thead>
<tr>
<th>stock ID</th>
<th>Industry</th>
<th>Sector</th>
<th>Symbol</th>
<th>Name</th>
<th>Buy</th>
<th>Sell</th>
<th>Timestamp</th>
</tr>
</thead>
<tbody>
{this.state.apiData.map((s) => (
<tr>
<td>{s.StockID}</td>
<td>{s.stock.industry}</td>
<td>{s.stock.sector}</td>
<td>{s.stock.symbol}</td>
<td>{s.stock.name}</td>
<td>{s.rates.buy}</td>
<td>{s.rates.sell}</td>
<td>{s.rates.timestamp}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
); // end of return
} // end of the else statement.
} // end of render()
} // end of App class
export default App;
Here is an example of one of my array objects:
{"stockData":
[
{
"stockID":1,
"stock":
{
"industry":"Investment Bankers/Brokers/Service",
"sector":"Finance",
"symbol":"JMP",
"name":"JMP Group LLC"
},
"rates":
{
"buy":12.6,
"sell":393.11,
"timestamp":"2024-06-05 19:12:01"
}
},
{
"stockID":2,
"stock":
{
"industry":"Investment Bankers/Brokers/Service",
"sector":"Finance",
"symbol":"USOI",
"name":"Credit Suisse AG"
},
"rates":
{
"buy":363.49,
"sell":14.15,
"timestamp":"2024-08-30 13:37:23"
}
},
Thanks!
It is a simple as:
<div>{this.state.apiData.length}</div>
Maybe the size property would help with that ?
So probably something like :
let objAmount = this.state.apiData.size
Or even just
this.state.apiData.size
, if you don't want to use a variable.
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.
//here is my code//
class TableList extends Component {
constructor(props) {
super(props);
//var totalPages = 100 / 10; // 10 page numbers
this.state = {
query: "",
countries: [],
searchString:[],
currentPageNumber: 1,
pageOfItems: [],
totalItems: 4,
itemsPerPage: 10
}
this.onChangePage = this.onChangePage.bind(this);
}
onChangePage(pageOfItems) {
this.setState({ pageOfItems: pageOfItems });
}
handleInputChange = (event) => {
this.setState({
query: event.target.value
},()=>{
this.filterArray();
})
}
handleSelect(number) {
console.log('handle select', number);
this.setState({currentPageNumber: number});
}
componentDidMount() {
const apiUrl = 'https://indian-cities-api-nocbegfhqg.now.sh/cities';
fetch(apiUrl)
.then(res => res.json())
.then(
(result) => {
this.setState({
countries: result,
searchString:result,
currentPageNumber:result.currentPageNumber,
totalItems: result.totalItems,
itemsPerPage: result.itemsPerPage
});
},
)
}
filterArray = () => {
let searchString = this.state.query;
let result = this.state.countries;
if(searchString.length > 0){
result = result.filter(searchString);
this.setState({
result
})
}
}
render() {
const { countries} = this.state;
let totalPages = Math.ceil(this.state.totalItems / this.state.numItemsPerPage);
return(
<div>
<div className="container">
</div>
<h2>countrie List</h2>
<form>
<input type="text" id="filter" placeholder="Search for..." onChange={this.handleInputChange}/>
</form>
<Table>
<Pagination
bsSize="medium"
items={totalPages}
activePage={this.state.currentPageNumber} onSelect={this.handleSelect.bind(this)}/>
<thead>
<tr>
<th>#ID</th>
<th>countrie Name</th>
<th>Code</th>
<th>States</th>
</tr>
</thead>
<tbody>
{countries.map(countrie => (
<tr key={countrie.City}>
<td>{countrie.sno}</td>
<td>{countrie.City}</td>
<td>{countrie.State}</td>
<td>{countrie.District}</td>
</tr>
))}
</tbody>
</Table>
</div>
)
}
}
export default TableList;
//the error coming is
Warning: Encountered two children with the same key, `Wadi`. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version
IN SHORT:
Search - filtered data stored in this.state.result - not used in render
this.setState({
result
})
as { result } is a short of { result: result } and this is good ... overwriting this.state.countries would result in source data loss (needs refetching)
render gets/make use of this.state.countries - always full dataset, not filtered by search, not divided by page ranges
You need to copy some data into this.state.result after fetching (not copy a countries reference)
Pagination - 'results' (not proper as above) records not subselected by range based on currentPage
Inspect state changes (check if properly working) in browser using react dev tools.
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.
I am retrieving table data from componentWillMount and showing it as UI. I want to trigger a fuction when the row/cell is clicked and return that value back to another PHP so that i can query the backend using that retrieved cell value.
index = 0,1,2,3 ( I have 4 columns )
row = data for each index.
I am not sure why this.state.testTime is not sending the correct value to handleClick function. Does anyone have any inputs on this? Thanks.
_handleClick: function (event) {
this.setState({testTime: event.target.value});
console.log(event.target.value);
var data = {
testTime: this.state.testTime
}
console.log("clicked");
$.ajax({
type: "GET",
crossDomain: true,
url: "http://localhost:8080/TEST/capture.php",
data: data,
success: function(data){
alert(data);
},
error:function(data)
{
alert("Data sending failed");
}
});
},
return (
<tbody>
{tableData.map((row, index) => {
return (
<tr key={"row_" + index} >
{row.map((cell, index) => {
return (
<td key={"cell_" + index} onClick={this._handleClick} value={this.state.testTime} >{cell}</td>
);
})}
</tr>
);
})}
</tbody>
)
setState is an async function and it accepts a callback as the second argument that will be executed once setState is completed and the component is re-rendered. So you either need to use event.target.value directly for the data variable or put your code into the callback:
this.setState({testTime: event.target.value} ,() => {
var data = {testTime: this.state.testTime}
...
$.ajax...
});