New programmer here learning ReactJS. I have some code that uses axios to make an HTTP request to get XMLData from a local file. Then on the response, I am using xml-js to turn that XML data into a JSON object. Then, I am taking that jsonObj and saving it to a state using setState().
I have a function renderTableRows() that is supposed to return JSX to display my JSON data on the browser. I destructured the state and try to console log from the renderTableRows() but when I try to access users.result.body I get
"TypeError: Cannot read property 'body' of undefined".
When I do it from the then() within the componentDidMount() I am able to access the data. I have also include an excerpt of the data I am reading at the bottom of the code.
I'd like to iterate using map() through all the row array attributes. Any help would be appreciated.
class Table extends Component {
constructor(props) {
super(props)
this.state = {
users: []
}
}
async componentDidMount() {
axios.get(XMLData, {
"Content-Type": "application/xml; charset=utf-8"
})
.then((response) => {
var jsonObj = convert.xml2js(response.data,{compact:true, spaces: 4});
this.setState({users:jsonObj});
//console.log(this.state.users.result.body.row[0].col[0]);
});
}
renderTableHeader = () => {
return <th>
<td>Division Full Path</td>
<td>Billable Hours</td>
<td>Vacation Hours Only</td>
<td>Total Hours</td>
</th>
}
renderTableRows = () => {
const {users} = this.state
console.log(users.result.body);
return <h1>Hello from table rows</h1>
}
render() {
//const { users } = this.state
return <table>
<thead>
<tr>
{this.renderTableHeader()}
</tr>
</thead>
<tbody>
<tr>
{this.renderTableRows()}
</tr>
</tbody>
</table>
}
"header": {
"col": [
{
"label": {
"_text": "Counter Source Date"
}
},
{
"label": {
"_text": "Employee Id"
}
},
{
"label": {
"_text": "Counter Hours"
}
},
{
"label": {
"_text": " Division Full Path"
}
},
{
"label": {
"_text": " Projects/Equip/Vessels\nBillable"
}
},
{
"label": {
"_text": "Counter Name"
}
}
]
}
"body": {
"row": [
{
"col": [
{
"_text": "01/01/2021"
},
{
"_text": "2183"
},
{
"_text": "8.00"
},
{
"_text": "01 - Fort Lauderdale/Salvage"
},
{
"_text": "No"
},
{
"_text": "Holiday"
}
]
}
]
}
Issue
The initial state doesn't match how it is accessed in renderTableRows.
this.state = {
users: []
}
Here this.state.users is an array, so this.state.users.result is undefined. This is fine until you then attempt to access a body property and the error TypeError: Cannot read property 'body' of undefined is thrown.
A Solution
You can either start with valid initial state:
this.state = {
users: {
result: {}
}
}
Or use a bunch of guard clauses in renderTableRows:
renderTableRows = () => {
const { users } = this.state
console.log(users.result && users.result.body);
return <h1>Hello from table rows</h1>
}
Or use Optional Chaining:
renderTableRows = () => {
const { users } = this.state
console.log(users.result?.body);
return <h1>Hello from table rows</h1>
}
Since you mention wanting to map through the rows the first option isn't what you want. If rendering rows it'll be something like:
renderTableRows = () => {
const {users} = this.state
return users.map(user => (....))
}
Update
I suggest setting your state to jsonObj.result properties, this is so you don't need to access the result property each render, it just shortens the access. Map this.state.users.headerColumns to the header columns and map this.state.rows to each row and additionally map the row columns.
class Table extends Component {
constructor(props) {
super(props);
this.state = {
users: {
headerColumns: [],
rows: [],
}
};
}
async componentDidMount() {
axios
.get(XMLData, {
"Content-Type": "application/xml; charset=utf-8"
})
.then((response) => {
var jsonObj = convert.xml2js(response.data, {
compact: true,
spaces: 4
});
this.setState({ users: {
headerColumns: jsonObj.header.col,
rows: jsonObj.body.row
} });
});
}
renderTableHeader = () => {
const { users: { headerColumns } } = this.state;
return (
<th>
{headerColumns.map(col => (
<td key={col.label._text}>{col.label._text}</td>
))}
<td>Total Hours</td>
</th>
);
};
renderTableRows = () => {
const { users: { rows } } = this.state;
return rows.map((row, index) => {
let computedTotal;
return (
<tr key={index}>
{row.col.map((value, index) => {
// compute time total from row data
return (
<td key={index}>{value}</td>
);
})}
<td>{computedTotal}</td>
</tr>
)});
};
render() {
return (
<table>
<thead>
<tr>{this.renderTableHeader()}</tr>
</thead>
<tbody>
{this.renderTableRows()}
</tbody>
</table>
);
}
}
Related
I am using an API to fetch some data. When the page loads it fetches some random data, but I want to allow the user to sort the data by clicking a button. I have made a function to sort these data from the API I am using. What I want to do now is: When the button to sort data is clicked, I want the new data to be replaced with the old data.
Here is my current code:
class Data extends React.Component {
constructor(props) {
super(props);
this.state = {
data: [],
offset: 0, perPage: 12 // ignore these two
};
}
// The random data that I want to be displayed on page load
receivedData() {
axios
.get(`https://corona.lmao.ninja/v2/jhucsse`)
.then(res => {
const data = res.data;
const slice = data.slice(this.state.offset, this.state.offset + this.state.perPage) // ignore this
const postData = slice.map(item =>
<tr key={Math.random()}>
<td>{item.province}, {item.country}</td>
<td>{item.stats.confirmed}</td>
<td>{item.stats.deaths}</td>
<td>{item.stats.recovered}</td>
</tr>
)
this.setState({
pageCount: Math.ceil(data.length / this.state.perPage), // ignore this
postData
})
});
}
// The data to be sorted when the "country" on the table head is clicked
sortData() {
axios
.get(`https://corona.lmao.ninja/v2/jhucsse`)
.then(res => {
const data = res.data;
var someArray = data;
function generateSortFn(prop, reverse) {
return function (a, b) {
if (a[prop] < b[prop])
return reverse ? 1 : -1;
if (a[prop] > b[prop])
return reverse ? -1 : 1;
return 0;
};
}
// someArray.sort(generateSortFn('province', true))
const tableHead = <tr>
<th onClick={() => someArray.sort(generateSortFn('province', true))}>Country</th>
<th>Confirmed Cases</th>
<th>Deaths</th>
<th>Recovered</th>
</tr>
this.setState({
tableHead
})
});
}
componentDidMount() {
this.receivedData()
this.sortData() // This function should be called on the "Country - table head" click
}
render() {
return (
<div>
<table>
<tbody>
{this.state.tableHead}
{this.state.postData}
</tbody>
</table>
</div>
)
}
}
export default Data;
Think a litte bit different. In the componentDidMount get you're Data in some form. Set it with setState only the raw Data not the html. Then resort the data on button click. React rerenders if the state changes automatically
class Data extends React.Component {
constructor(props) {
super(props);
this.state = {
data
}
}
getData() {
fetchData('url').then(res) {
this.setState({data: res.data})
}
}
componentDidMount() {
this.getData()
}
sort() {
let newSorted = this.state.data.sort //do the sorting here
this.setState({data: newSorted})
}
render() {
return() {
<table>
<tablehead><button onClick={this.sort.bind(this)}/></tablehead>
{this.state.data.map(data => {
return <tablecell>{data.name}</tablecell>
})}
</table>
}
}
}
I found a tutorial to implement DataTable in React App but I have no idea how can I custom my tr, td, colspan etc...
My DataTable.js is :
import React, { Component } from 'react';
const $ = require('jquery');
$.DataTable = require('datatables.net');
class App extends Component {
componentDidMount() {
this.$el = $(this.el);
this.$el.DataTable({
data: this.props.data,
columns: this.props.columns
})
}
render() {
return (
<div className="table-responsive">
<table className="table" ref={el => this.el = el}>
</table>
</div>
);
}
}
And after in my Test.js :
import React, { Component } from 'react';
import DataTable from './DataTable';
import axios from 'axios';
class Test extends Component {
constructor(props) {
super(props);
this.state = {
data: [],
columns: [
{ title: "Id" },
{ title: "Category" },
{ title: "Title" },
{ title: "Command 1" },
{ title: "Command 2" },
{ title: "Command 3" }
]
}
}
componentDidMount() {
this._getFilteredItems();
}
_getFilteredItems = () => {
axios.post('http://localhost:8080/items/category', { category: "category1" })
.then((res) => {
var test = res.data.map((e) => Object.values(e)); // to transform objet in array
this.setState({ data: test });
})
.catch(error => { console.log(error) });
}
display = () => {
if(this.state.data.length > 0){
return (
<DataTable
data={this.state.data}
columns={this.state.columns}>
</DataTable>
);
}
}
render() {
return (
<div>
{this.display()}
</div>
);
}
}
export default Test;
My data received from my backend is like this :
[
["5e9c231facad1424801f5167", "category1", "title", "command1", "command2", "command3"],
["5e9c2337acad1424801f58ce", "category1", "title", "command1", "command2", "command3"],
["5eaa105b82d1130017d31dbe", "category1", "title", "command1", "command2", "command3"],
]
The thing is I would like to custom my tr, td, colspan etc... I mean, I would like for example put the title with a colspan="5" and my command1, command2 and command3 in the same td.
Do you have any idea how can I do that ? Thanks
I found, I simply initialized my table as I wanted in the render instead of using props like the tutorial mentionned it :
import React, { Component } from 'react';
import axios from 'axios';
const $ = require('jquery');
$.DataTable = require('datatables.net');
class Test extends Component {
constructor(props) {
super(props);
this.state = {
data: []
}
}
componentDidMount() {
this._getFilteredItems();
}
_getFilteredItems = () => {
axios.post('http://localhost:8080/items/category', { category: "category1" })
.then((res) => {
this.setState({data: res.data});
this.$el = $(this.el);
this.$el.DataTable({});
})
.catch(error => { console.log(error) });
}
render() {
return (
<div className="table-responsive">
<table className="table" ref={el => this.el = el}>
<thead>
<tr>
<th>Title</th>
<th>Commands</th>
</tr>
</thead>
<tbody>
{this.state.data.map((elmt, i) => (
<tr key={i}>
<td>
{elmt.title}
</td>
<td>
{elmt.command1} <br/>
{elmt.command2} <br/>
{elmt.command3} <br/>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
}
export default Test;
I'm using Datatable Library to draw table easily.
And I got a data with Fetch API and render to table and It works well. But I don't know why DataTable Funcions like sorting, searching, showing options.
As you see, get data from API and render to HTML are works well, but when I click sort or search function it changes to this.
Also Another functions like interval the data from API every 10 seconds and render to table are works well.
It seems that there are some problem in initial state.
import React, { Component } from 'react';
import './PostContainer.css';
class PostContainer extends Component {
constructor(props) {
super(props);
this.state = {
tableData: {
status: '0000',
data: {
loading: { sell_price: 'loading', volume_7day: 'loading' },
},
},
};
}
async componentDidMount() {
this.getData();
this.interval = setInterval(() => {
this.getData();
}, 10000);
}
getData() {
fetch('https://api.bithumb.com/public/ticker/all')
.then(res => {
const data = res.json();
return data;
})
.then(res => {
this.setState({
tableData: res,
});
})
.catch(error => {
console.error(error);
});
}
componentWillUnmount() {
clearInterval(this.interval);
}
render() {
let data = this.state.tableData;
let chart = [];
console.log(data);
if (data.status === '0000') {
delete data.data['date'];
for (let [key, value] of Object.entries(data.data)) {
chart.push(
<tr key={key}>
<td>{key}</td>
<td>{value.sell_price}</td>
<td>{value.volume_7day}</td>
</tr>
);
}
} else if (
data.status === '5500' ||
data.status === '5600' ||
data.status === '5900'
) {
this.setState({
tableData: {
data: {
ERROR: {
sell_price: 'ERROR with API',
volume_7day: 'ERROR with API',
},
},
},
});
}
return (
<div className="Post">
<table id="table" className="table table-striped table-bordered">
<thead>
<tr>
<th>Coin Name</th>
<th>Current Price</th>
<th>Volume</th>
</tr>
</thead>
<tbody>{chart}</tbody>
</table>
</div>
);
}
}
export default PostContainer;
Can access to DEMO directly. I uploaded to Github Pages.
I can think of 2 issue to look for
a.
If you see in screenshot there is no Pagination.
When I try load DEMO with Developer Console open.
It works fine and you will see Pagination which will show 10 record at a time.
Check your code for Datatable initialization code in index.js
$('#table').DataTable({
order: [[1, 'desc']],
});
Make sure above code is called after Data is loaded in HTML
b.
Your state value is not updated to actual values while sorting
tableData: {
status: "0000",
data: {
loading: {
sell_price: "loading",
volume_7day: "loading"
}
}
}
Based on this previous questions I made (Show fetch results in render return() in React.js), from which I received json results, I now need to count the number of sofas that each brand has. For example, Brand X has 2 occurences and Brand Y has 3043.
I get the brand from one sofa by calling myUrlApi + /couch-model on fetch and the json is something like what you can see in the picture below.
Has you can see each sofa has associated to itself a brand. What I want to count is how many sofa each brand has.
I'll put my current code here:
export class Main extends React.Component {
constructor(props) {
super(props);
this.state = {
token: {},
isLoaded: false,
models: []
};
}
componentDidMount() {
/*code to generate token, not needed for the purpose of the question*/
fetch(url + "/couch-model/?limit=9", {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
Authorization: "JWT " + JSON.parse(localStorage.getItem("token")).token
}
})
.then(res => {
if (res.ok) {
return res.json();
} else {
throw Error(res.statusText);
}
})
.then(json => {
this.setState(
{
models: json.results
},
() => {}
);
});
}
render() {
const { isLoaded, models } = this.state;
if (!isLoaded) {
return <div>Loading...</div>;
} else {
return (
<div>
{models.map(model => (
<a href="/sofa" key={model.id}>
<div className="Parcelas">
<img src={img_src} className="ParcImage" alt="sofa" />
<h1>Sofá {model.name}</h1>
<h2>
1,200<span>€</span>
</h2>
<p
className="Features"
dangerouslySetInnerHTML={{ __html: model.description }}
/>
<button className="Botao">
<p className="MostraDepois">Ver Detalhes</p>
<span>+</span>
</button>
<img
src="../../img/points.svg"
className="Decoration"
alt="points"
/>
</div>
</a>
))}
</div>
);
}
}
}
Hope I was clear, ask if you have any doubt.
if your results look like this as you said in your post :
{
results: [
{
brand: { name: "Brand-A", image: "", etc: "..." },
category: "A",
code: "AAA",
name: "SofaA",
price: 1200
},
{
brand: { name: "Brand-A", image: "", etc: "..." },
category: "A",
code: "AAA",
name: "SofaB",
price: 1200
},
{
brand: { name: "Brand-B", image: "", etc: "..." },
category: "A",
code: "AAA",
name: "SofaC",
price: 1200
}
]
}
You can add a state property like sofasPerBrand initialized to {}
constructor(props) {
super(props);
this.state = {
token: {},
isLoaded: true,
models: [],
sofasPerBrand: {}
};
}
And add in the setState function in componentDidMount the RIYAJ KHAN reduce function like this :
this.setState(
{
models: json.results,
sofasPerBrand: json.results.reduce((coundData, sofa, index) => {
if (!!coundData[sofa.brand.name]) {
coundData[sofa.brand.name] += 1;
} else {
coundData[sofa.brand.name] = 1;
}
return coundData;
}, {})
},
() => { }
);
then you can declare it in your render function :
const { isLoaded, models, sofasPerBrand } = this.state;
and use it like that any where :
<ul>
{Object.keys(sofasPerBrand).map(brand=>(
<li>{brand} : {sofasPerBrand[brand]}</li>
))}
</ul>
One can use javascript#reducers for it.
models.reduce((coundData,sofa,index)=>{
if(!!coundData[sofa.brand.name]){
coundData[sofa.brand.name] +=1;
}else{
coundData[sofa.brand.name]=1;
}
return coundData;
}, {})
I am fetching data from a API,then displaying it to the page. I have achieved that,
Now I want to build a next and previous button to render the next page of information.
One of the data returned is metadata to links that can be attached to the base url. I got the data and updated it in my state as:
articlePages: []
the data is structured as :
"metadata": {
"pagination": {
"next_page": "/api/articles/ios_index?page=2",
"current_page": "/api/articles/ios_index?page=1",
"previous_page": "/api/articles/ios_index?page=0"
}
}
How should I build the functions for previous and next, so that they attach the right string to the base url, then fetch the new data?
Here is the response I receive then I update my state:
Response Format:
"metadata": {
"pagination": {
"next_page": "/api/articles/ios_index?page=2",
"current_page": "/api/articles/ios_index?page=1",
"previous_page": "/api/articles/ios_index?page=0"
}
}
"data" :{
"id": 713,
"url": "https:sample.-sale",
"title": "The World According to Eddie Huang",
"published_at": "2017-08-29T04:00:00.000Z",
"published": true,
"hero": "https://d1qz9pzgo5wm5k./CPG9crJHRqSPKQg9jymd",
"listings": [],
"tag_list": [
"eddie-huang",
"television"
],
"franchise": "The Drop",
"slug": "eddie-huang-interview-sale",
"author": "Lawrence Schlossman",
"content_type": "long",
"position": "feature"
}
Here is a snippet of my code, any help is appreciated :
import React from 'react';
import axios from 'axios';
export default class ArticleApi extends React.Component {
constructor() {
super();
this.state = {
blogs: "",
articlePages: []
}
}
fetchData = () => {
axios.get(`https:sample.sale/api/articles/ios_index`)
.then(res => {
this.setState({
blogs: res.data.data,
blogPages: res.data.metadata
})
})
.catch(error => {
return ('Looks like there was a problem: \n', error);
});
}
componentDidMount() {
this.fetchData()
}
previousPage = () => {
axios.get(`https:sample.sale/api/articles/ios_index${this.state.blogPages.pagination.previous_page}`)
.then(res => {
this.setState({
blogs: res.data.data,
blogPages: res.data.metadata
})
})
.catch(error => {
return (error);
});
}
nextPage = () => {
axios.get(`https:sample.sale/api/articles/ios_index${this.state.blogPages.pagination.next_page}`)
.then(res => {
this.setState({
blogs: res.data.data,
blogPages: res.data.metadata
})
})
.catch(error => {
return (error);
});
}
render() {
let feed = "Loading...";
if (this.state.blogs) {
feed = this.state.blogs.map((ele, idx) => {
return (
<div key={idx} >
<div className="articleContent">
<p><strong>{ele.franchise}</strong></p>
<h1 className="title"> {ele.title}</h1>
</div>
</div>
)
})
}
return (
<div>
<h3 FEED</h3>
{feed}
<button onClick={this.previousPage}>Previous Page</button>
<button onClick={this.nextPage}>Next Page</button>
</div>
)
}
}
At present you are building a strange URL for both the next and previous page functions:
axios.get(`https:sample.sale/api/articles/ios_index${this.state.blogPages.pagination.next_page}`)
// but this.state.blogPages.pagination.next_page is equal to "/api/articles/ios_index?page=2", right?
// So if we replace the variable with its value, your url actually looks something like this:
axios.get('https:sample.sale/api/articles/ios_index/api/articles/ios_index?page=2')
The correct call should look like:
axios.get(`https:sample.sale${this.state.blogPages.pagination.next_page}`)
And similarly for previous page.