Home > other >  react merging two components and sorting data by date and time (newest at top oldest and bottom)
react merging two components and sorting data by date and time (newest at top oldest and bottom)

Time:05-07

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;
        // ...
    }
}
  • Related