Ad

Passing An Identifier To A Module In React

- 1 answer

Apologies, I probably have all of my terminology wrong here, but I am chunking up a React app into modules and am trying to combine two modules which do the same thing and come into an issue.

Specifically, I am trying to sum up totals of both income and expenditure, and have been calling two different react classes via:

<ExpenditureTotal type={this.state.cashbook.expenditure} addTotal={this.addTotal} /> and another <IncomeTotal... which does the same).

The Class is as follows (expenditure shown, but income is the same:

/* Expenditure Running Total */
var ExpenditureTotal = React.createClass({
  render: function() {

    var expIds = Object.keys(this.props.expenditure);
    var total = expIds.reduce((prevTotal, key) => {
      var expenditure = this.props.expenditure[key];
      return prevTotal + (parseFloat(expenditure.amount));
    }, 0);

    this.props.addTotal('expenditure', total);

    return(
      <h2 className="total">Total: {h.formatPrice(total)}</h2>
    );
  }
});

I wanted to combine the two by making it more generic, so I made the following:

/* Cashflow Running Total */
var TotalCashflow = React.createClass({
  render: function() {
    var expIds = Object.keys(this.props.type);
    var total = expIds.reduce((prevTotal, key) => {
      var type = this.props.type[key];
      return prevTotal + (parseFloat(type.amount));
    }, 0);

    this.props.addTotal('type', total);

    return(
      <h2 className="total">Total: {h.formatPrice(total)}</h2>
    );
  }
});

The only reason it doesn't work properly is when I add the summed total to the relevant state object which for this is income or expenditure in totals: (totals: { income: val, expenditure: val}). Where I was previously explicitly specifying 'expenditure' or 'income' in the module, (seen above in ExpenditureTotal as this.props.addTotal('expenditure', total);, I am not sure in React how to pass the total value to the relevant place - it needs to read either 'expenditure' or 'income' to populate the correct state totals key.

Apologies if this is a bit garbled, struggling to explain it clearly.

Thank you :)

Update: App component in full:

import React from 'react';

// Firebase
import Rebase from 're-base';
var base = Rebase.createClass("FBURL")

import h from '../helpers';

import Expenditure from './Expenditure';
import Income from './Income';
import TotalCashflow from './TotalCashflow';
import AddForm from './AddForm';
import Available from './Available';

var App = React.createClass({

  // Part of React lifecycle
  getInitialState: function() {
    return {
      cashbook: {
        expenditure: {},
        income: {}
      },
      totals: {},
      available: {}
    }
  },

  componentDidMount: function() {
    // Two way data binding
    base.syncState('cashbook', {
      context: this,
      state: 'cashbook'
    });
  },

  addExpenditure: function(expenditure) {
    var timestamp = (new Date()).getTime();
    // update state object
    this.state.cashbook.expenditure['expenditure-' + timestamp] = expenditure;
    // set state
    this.setState({
      cashbook: { expenditure: this.state.cashbook.expenditure }
    });
  },
  addIncome: function(income) {
    var timestamp = (new Date()).getTime();
    // update state object
    this.state.cashbook.income['income-' + timestamp] = income;
    // set state
    this.setState({
      cashbook: { income: this.state.cashbook.income }
    });
  },
  removeExpenditure: function(key) {
    this.state.cashbook.expenditure[key] = null;
    this.setState({
      cashbook: { expenditure: this.state.cashbook.expenditure }
    });
  },
  renderExpenditure: function(key) {
    var details = this.state.cashbook.expenditure[key];
    return(
      <tr className="item" key={key}>
        <td><strong>{details.name}</strong></td>
        <td><strong>{h.formatPrice(details.amount)}</strong></td>
        <td>{details.category}</td>
        <td>{details.type}</td>
        <td>{details.date}</td>
        <td><button className="remove-item" onClick={this.removeExpenditure.bind(null, key)}>Remove</button></td>
      </tr>
    );
  },
  removeIncome: function(key) {
    this.state.cashbook.income[key] = null;
    this.setState({
      cashbook: { income: this.state.cashbook.income }
    });
  },
  renderIncome: function(key) {
    var details = this.state.cashbook.income[key];
    return(
      <tr className="item" key={key}>
        <td><strong>{details.name}</strong></td>
        <td><strong>{h.formatPrice(details.amount)}</strong></td>
        <td>{details.category}</td>
        <td>{details.type}</td>
        <td>{details.date}</td>
        <td><button className="remove-item" onClick={this.removeIncome.bind(null, key)}>Remove</button></td>
      </tr>
    );
  },
  listInventory: function() {
    return
  },
  addTotal: function(type, total) {
    this.state.totals[type] = total;
  },
  render: function() {

    return(
      <div className="cashbook">

        <Expenditure
          cashbook={this.state.cashbook.expenditure}
          renderExpenditure={this.renderExpenditure} />

        <TotalCashflow
          type={this.state.cashbook.expenditure}
          addTotal={this.addTotal}
          identifier='expenditure'
          />

        <AddForm addCashflow={this.addExpenditure} />

        <Income
          cashbook={this.state.cashbook.income}
          renderIncome={this.renderIncome} />

        <TotalCashflow
          type={this.state.cashbook.income}
          addTotal={this.addTotal}
          identifier='income'
          />

        <AddForm addCashflow={this.addIncome} />

        <Available totals={this.state.totals} />

      </div>
    );
  }
});

export default App;
Ad

Answer

If I got you correctly, you want to have a component, that renders the totals of 'expenditure' and/or 'income'. Notice, you should not modify the state within the render function, as these will trigger over and over again, with each state change it is invoking itself.

Instead, try to compute the necessary data from within the parent components componentDidMount/componentDidUpdate, so that the TotalCashflow can be served the data, without it needing to do any more logic on it.

This might be the parent component:

/* Cashflow-Wrapper */
var TotalCashflowWrapper = React.createClass({
  componentDidMount: function(){
    this.setState({
        income: this.addTotal(/*x1*/)
        expenditure: this.addTotal(/*x1*/)
    });
    /*x1: hand over the relevant 'type' data that you use in the child components in your example*/
  }
  addTotal: function(type) {
    var expIds = Object.keys(type);
    return expIds.reduce((prevTotal, key) => {
       var x = type[key];
       return prevTotal + (parseFloat(x.amount));
    }, 0);
  }
  render: function() {
    return(
      <div>
         <TotalCashflow total={this.state.expenditure}/>
         <TotalCashflow total={this.state.income}/>
      </div>
    );
  }
});

This would be your total component:

   /* Cashflow Running Total */
    var TotalCashflow = React.createClass({
      render: function() {
        return(
          <h2 className="total">Total: {h.formatPrice(this.props.total)}</h2>
        );
      }
    });

Edit

There are a few issues with your App component, but first of all, here is how you could redesign/simplify it. You should also move the markup from renderExpenditure and renderIncome to your Income/Expenditure components. I've been using ES6, as I've noticed you're already using arrow functions.

var App = React.createClass({
   getInitialState: function () {
      return {
         cashbook: {
            expenditure: {},
            income: {}
         },
         totals: {},
         available: {}
      }
   },

   componentDidMount: function () {
      // Two way data binding
      base.syncState('cashbook', {
         context: this,
         state: 'cashbook'
      }).then(()=> {
         // after syncing cashbook, calculate totals
         this.setState({
            totals: {
               income: this.getTotal(this.state.cashbook.income),
               expenditure: this.getTotal(this.state.cashbook.expenditure),
            }
         });
      });
   },

   // Get total of obj income/expenditure
   getTotal: function (obj) {
      var expIds = Object.keys(obj);
      return expIds.reduce((prevTotal, key) => {
         var type = obj[key];
         return prevTotal + (parseFloat(type.amount));
      }, 0);
   },

   addCashflow: function (identifier, amount) {
      var timestamp = (new Date()).getTime();
      // clone cashbook and clone cashbook[identifier] and set cashbook[identifier][identifier + '-' + timestamp] to amount
      var cashbook = {
         ...this.state.cashbook,
         [identifier]: {
            ...this.state.cashbook[identifier],
            [identifier + '-' + timestamp]: amount
         }
      };
      // set state
      this.setState({
         cashbook: cashbook,
         // Update totals
         totals: {
            ...this.state.totals,
            [identifier]: this.getTotal(cashbook[identifier])
         }
      });
   },

   removeCashflow: function (identifier, key) {
      // clone cashbook and clone cashbook[identifier]
      var cashbook = {...this.state.cashbook, [identifier]: {...this.state.cashbook[identifier]}};
      delete cashbook[identifier][key];
      this.setState({
         cashbook: cashbook,
         totals: {
            ...this.state.totals,
            // Update totals
            [identifier]: this.getTotal(cashbook[identifier])
         }
      });
   },

   render: function () {
      return (
         <div className="cashbook">
            <Expenditure cashbook={this.state.cashbook.expenditure} removeCashflow={(key)=>this.removeCashflow('expenditure', key)}/>
            <TotalCashflow total={this.state.cashbook.expenditure} identifier='expenditure'/>
            {/*or drop TotalCashflow and do <h2 className="total">Total: {h.formatPrice(this.state.totals.expandature)}</h2>*/}
            <AddForm addCashflow={(amount)=>this.addCashflow('expenditure', amount)}/>
            <Income cashbook={this.state.cashbook.income} removeCashflow={(key)=>this.removeCashflow('income', key)}/>
            <TotalCashflow type={this.state.cashbook.income} identifier='income' />
            <AddForm addCashflow={(amount)=>this.addCashflow('income', amount)}/>
            <Available totals={this.state.totals}/>
         </div>
      );
   }
});

export default App;

Issues with your current App component

Issue: You are not removing the key, you're just setting it to null; this could lead to exceptions when iterating Object.keys() and you're expecting the values to be numbers, like when calculating the totals

removeIncome: function(key) {
      // this.state.cashbook.income[key] = null;
      delete this.state.cashbook.income[key]
      this.setState({
         cashbook: { income: this.state.cashbook.income }
      });
   },

Bad design: You're defining the markup of a child component within your parent component, although it seems unnecessary

renderExpenditure: function(key) {
    var details = this.state.cashbook.expenditure[key];
    return(
      <tr className="item" key={key}>
        <td><strong>{details.name}</strong></td>
        <td><strong>{h.formatPrice(details.amount)}</strong></td>
        <td>{details.category}</td>
        <td>{details.type}</td>
        <td>{details.date}</td>
        <td><button className="remove-item" onClick={this.removeExpenditure.bind(null, key)}>Remove</button></td>
      </tr>
    );
  },

This could easily be moved to your Expenditure/Income component.

Issues: Mutating state, Overwriting cashbook in state and loosing expenditure/income

addExpenditure: function(expenditure) {
      var timestamp = (new Date()).getTime();
      // You are mutating the state here, better clone the object and change the clone, then assign the cloned
      this.state.cashbook.expenditure['expenditure-' + timestamp] = expenditure;
      // You are overwriting cashbook with an object, that only contains expenditure, thus loosing other properties like income
      this.setState({
         cashbook: { expenditure: this.state.cashbook.expenditure }
      });
   }

// Fixed

addExpenditure: function(expenditure) {
      var timestamp = (new Date()).getTime();
      // clone object
      var cashbook = Object.assign({}, this.state.cashbook);
      cashbook.expenditure = Object.assign({}, cashbook.expenditure);
      // define latest expenditure
      cashbook.expenditure['expenditure-' + timestamp] = expenditure;
      // set state
      this.setState({
         cashbook: cashbook
      });
   }

// ES6

addExpenditure: function(expenditure) {
      var timestamp = (new Date()).getTime();
      this.setState({
         cashbook: {
          ...this.state.cashbook,
          expenditure: {
            ...this.state.cashbook.expenditure,
            ['expenditure-' + timestamp]: expenditure
          }
         }
      });
   }

You would be better off, if you'd flatten your state and cashbook object so instead of

this.state = {
         cashbook: {
            expenditure: {},
            income: {}
         },
         totals: {},
         available: {}
      }

having

this.state = {
         expenditure: {},
         income: {}
         totals: {},
         available: {}
      }

so you could just

addExpenditure: function(expenditure) {
      var timestamp = (new Date()).getTime();
      var exp = Object.assign({}, this.state.cashbook.expenditure);
      exp['expenditure-' + timestamp] = expenditure;
      this.setState({
         expenditure: exp
      });
   }

or es6

addExpenditure: function(expenditure) {
      var timestamp = (new Date()).getTime();
      this.setState({
         expenditure: {
            ...this.state.cashbook.expenditure,
            ['expenditure-' + timestamp]: expenditure
         }
      });
   }

of course you'd need to update the rebase binding and your models, dunno if this is something you want

componentDidMount: function() {
      base.syncState('expenditure', {
         context: this,
         state: 'expenditure'
      });
      base.syncState('income', {
         context: this,
         state: 'income'
      });
   }
Ad
source: stackoverflow.com
Ad