I have codes below
IncomeExpenseSummary.js
import axios from 'axios';
import React, { Component } from 'react'
import ExpenseRow from './ExpenseRow';
import IncomeRow from './IncomeRow';
class IncomeExpenseSummary extends Component {
constructor(props) {
super(props);
this.state = {
expenses:[],
incomes:[],
}
}
componentDidMount(){
this.getExpenseList();
this.getIncomeList();
}
getExpenseList =() =>{
let self=this;
axios.get('/get/expenses',{
params:{_limit: 3}
}).then(function(response)
{self.setState({
expenses:response.data
});
});
}
getIncomeList =() =>{
let self=this;
axios.get('/get/incomes',{
params:{_limit: 3}
}).then(function(response)
{self.setState({
incomes:response.data
});
});
}
render(){
return(
<div>
<table className="table">
{/* <toast/> */}
<thead>
<tr>
<th scope="col">Date</th>
<th scope="col">Name</th>
<th scope="col">Price</th>
<th scope="col">Category</th>
</tr>
</thead>
<tbody>
//this part needs to be modified
{this.state.expenses.slice(0, 4).map(function(x, i){
return <ExpenseRow key={i} data={x} />
})}
{this.state.incomes.slice(0, 4).map(function(x, i){
return <IncomeRow key={i} data={x} />
})}
</tbody>
</table>
</div>
);
}
}
export default IncomeExpenseSummary;
IncomeRow.js
import React, { Component } from 'react'
class IncomeRow extends Component {
render(){
return (
<tr className='alert alert-success'>
<th>{this.props.data.created_at}</th>
<td>{this.props.data.title}</td>
<td>{this.props.data.amount}</td>
<td>{this.props.data.category}</td>
</tr>
)
}
}
export default IncomeRow;
ExpenseRow.js
import React, { Component } from 'react'
class ExpenseRow extends Component {
render(){
return (
<tr className='alert alert-danger'>
<th>{this.props.data.created_at}</th>
<td>{this.props.data.title}</td>
<td>{this.props.data.amount}</td>
<td>{this.props.data.category}</td>
</tr>
)
}
}
export default ExpenseRow;
The problem is it prints rows of expenses and then rows of incomes (i.e posts from expenses are printed first then after them incomes are printed). I need them to be mixed in and printed accordingly by date and time (newest post goes to top and oldest at bottom)
CodePudding user response:
To minimize your code change, I'd propose to add another state called row
and use componentDidUpdate
to listen to expenses
and incomes
states change for row
state update.
import axios from 'axios';
import React, { Component } from 'react'
import ExpenseRow from './ExpenseRow';
import IncomeRow from './IncomeRow';
class IncomeExpenseSummary extends Component {
constructor(props) {
super(props);
this.state = {
expenses:[],
incomes:[],
rows: []
}
}
componentDidUpdate(prevProps, prevState) {
//trigger when incomes or expenses change
if(prevState.incomes !== this.state.incomes || prevState.expenses !== this.state.expenses) {
//add type to recognize `income` in mixed list
const updatedIncomes = [...this.state.incomes].map(income => ({...income, type: 'income'}))
//add type to recognize `expense` in mixed list
const updatedExpenses = [...this.state.expenses].map(expense => ({...expense, type: 'expense'}))
//sort all rows with the mixed list
const sortedRows = [...updatedExpenses, ...updatedIncomes].sort((a,b) => new Date(b.created_at) - new Date(a.created_at))
this.setState({ rows: sortedRows })
}
}
componentDidMount(){
this.getExpenseList();
this.getIncomeList();
}
getExpenseList = () =>{
//if you use an arrow function, you don't need to pass variable for `this`
axios.get('/get/expenses',{
params:{_limit: 3}
}).then((response) => {
this.setState({
expenses: response.data
});
});
}
getIncomeList =() =>{
//if you use an arrow function, you don't need to pass variable for `this`
axios.get('/get/incomes',{
params:{_limit: 3}
}).then((response) => {
this.setState({
incomes: response.data
});
});
}
render(){
return(
<div>
<table className="table">
{/* <toast/> */}
<thead>
<tr>
<th scope="col">Date</th>
<th scope="col">Name</th>
<th scope="col">Price</th>
<th scope="col">Category</th>
</tr>
</thead>
<tbody>
{this.state.rows.map((row) => row.type === 'expense' ? <ExpenseRow data={row} key={row.title}/> : <IncomeRow data={row} key={row.title}/>)}
</tbody>
</table>
</div>
);
}
}
export default IncomeExpenseSummary;
CodePudding user response:
If you are aren't worried about rendering duplicate data, this should work for you. I simply merged the two states, sorted them by date, and passed that to your new combined component ExpenseIncomeRow
. If you are actually trying to merge the data based on key/value
properties, then this solution may not be ideal for you.
import axios from 'axios';
import React, { Component } from 'react'
import ExpenseIncomeRow from './ExpenseIncomeRow';
class IncomeExpenseSummary extends Component {
constructor(props) {
super(props);
this.state = {
expenses:[],
incomes:[],
}
}
componentDidMount(){
this.getExpenseList();
this.getIncomeList();
}
sortByDate = (data) => {
data.sort((a, b) => new Date(b['created_at']) - new Date(a['created_at']))
}
getExpenseList =() =>{
let self=this;
axios.get('/get/expenses',{
params:{_limit: 3}
}).then(function(response)
{self.setState({
expenses: response.data
});
});
}
getIncomeList =() =>{
let self=this;
axios.get('/get/incomes',{
params:{_limit: 3}
}).then(function(response)
{self.setState({
incomes: response.data
});
});
}
render(){
return(
<div>
<table className="table">
{/* <toast/> */}
<thead>
<tr>
<th scope="col">Date</th>
<th scope="col">Name</th>
<th scope="col">Price</th>
<th scope="col">Category</th>
</tr>
</thead>
<tbody>
{this.sortByDate(this.state.expenses.slice(0, 4).concat(this.state.incomes.slice(0, 4)).map(function(x, i){
return <ExpenseIncomeRow key={i} data={x} />
}))}
</tbody>
</table>
</div>
);
}
}
export default IncomeExpenseSummary;
Here is the new component:
import React, { Component } from 'react'
class ExpenseIncomeRow extends Component {
render(){
return (
<tr className='alert alert-success'>
<th>{this.props.data.created_at}</th>
<td>{this.props.data.title}</td>
<td>{this.props.data.amount}</td>
<td>{this.props.data.category}</td>
</tr>
)
}
}
export default ExpenseIncomeRow;
I have changed your imports
but you will actually have to set up a new component on your end for it to work properly.
CodePudding user response:
I suggest adding a zip()
helper:
// Zip array arguments together.
//
// zip([1, 2, 3], [4, 5, 6])
// //=> [[1, 4], [2, 5], [3, 6]]
// zip([1], [2, 3], [])
// //=> [[1, 2, undefined], [undefined, 3, undefined]]
//
function zip(...arrays) {
if (!arrays.length) return;
const length = Math.max(...arrays.map(array => array.length));
return Array.from({ length }, (_, i) => arrays.map(array => array[i]));
}
If you are already using a library with helper functions (Underscore.js, Lodash, Ramda, etc.), chances are that this function is available through it.
You can then use it within your JSX in the following manner:
// moved taking the first 3 elements up, to simplify the JSX
const take = (n, array) => array.slice(0, n 1);
const expenses = take(3, this.state.expenses);
const incomes = take(3, this.state.incomes);
// ...
<tbody>
{zip(expenses, incomes).map(([expense, income], i) => (
<>
<ExpenseRow key={i} data={expense} />
<IncomeRow key={i} data={income} />
</>
))}
</tbody>
This solution assumes the HTTP request returns the items in the correct order. If this isn't the case, you'll need to sort both the arrays first.
If the length of expenses
and incomes
can vary. Say expenses
has length 3
and incomes
has length 1
. Then you'll probably want to add an additional check for both expenses
and incomes
(example only shows expenses
).
{expense && <ExpenseRow key={i} data={expense} />}
Or you could return null
in ExpenseRow
if no data is provided.
class ExpenseRow extends Component {
render() {
if (!this.props.data) return null;
// ...
}
}