React練習 – 割り勘計算アプリ(3)

サンプル開発

前回の投稿では、割り勘計算アプリをコンポーネント化し、HTMLがレンダリングされるところまで実装した。

  • BillSplitter …親コンポーネント。状態保持や計算処理を司る
  • Header …子コンポーネント。合計金額などの表示や入力を担当する
  • Detail …子コンポーネント。各参加者の支払額などの表示や入力を担当する。

現時点での画面イメージは以下。

現在の画面

Reactではイベントハンドラが紐付かない入力要素はreadonly扱いなので、まずはヘッダの支払額要素にイベントハンドラを紐付ける。

class Header extends Component {
  render () {
    return (
      <div>
        <div>
          <label>支払額:
            <input type="text"
              value={this.props.payment}
              onChange={e => this.handlePaymentChange(e)} />
          </label>
        </div>

9行目のonchangeで、ハンドラメソッドの呼び出しを記述した。ハンドラメソッドは、Headerコンポーネントのメソッドとして定義したもの。

  handlePaymentChange (e) {
    const payment = Number(e.target.value);
    if (payment) {
      this.props.onChange(payment);
    }
  }

親コンポーネントから渡されるコールバック関数(プロパティはonChange)を呼び出して処理を移譲する。

そして親コンポーネント。

class BillSplitter extends Component {

  constructor(props) {
    super(props);
    this.state = {
      payment: 0, //支払額
      total: 0, //合計額
      remainder: 0, //端数
      participants: new Array(3).fill().map(() => ({name: '', kind: 'split', amount: 0})), //参加者
    };
    this.handlePaymentChange = this.handlePaymentChange.bind(this);
  }

constructorに、ハイライトした行を追加した。
stateの初期値設定と、ハンドラメソッドのバインド。

ハンドラメソッドの実体は以下。

 handlePaymentChange (payment) {
    // 金額再計算
    const splitAmount = this.calcSplitAmount(payment);
    const participants = this.state.participants.map(p => {
      const amount = p.kind === 'fix' ? p.amount : splitAmount;
      return Object.assign({}, p, {amount: amount});
    });
    const total = participants.map(p => p.amount).reduce((a, x) => a += x, 0);
    const remainder = payment - total;
    // 状態更新
    this.setState({
      payment: payment,
      total: total,
      remainder: remainder,
      participants: participants,
    });
  }

前半では、子コンポーネントから受け取った新しい支払額と現在stateに保持しているデータから割り勘計算を行っている。
後半(11-16行目)では状態の更新を行っている。

Reactでの状態更新のポイント:

  • 直接stateをアップデートしてはいけない
  • 新しいstateオブジェクトを生成し、setStateメソッドを呼び出すことで更新する

次にrenderメソッド。

  render () {
    const details = this.state.participants.map(person => {
      return <Detail {...person} />
    })
    return (
      <>
        <Header
          payment={this.state.payment}
          total={this.state.total}
          remainder={this.state.remainder}
          participantsNumber={this.state.participants.length}
          onChange={this.handlePaymentChange}
        />
        {details}
      </>
    );

Headerで発生したchangeイベントをハンドルするためのコールバックの定義を12行目で行っている。もしここをアロー関数の書き方にすれば、おまじないのようなconstructorでのバインド処理は不要となる。

これで、ヘッダーの支払額の変更に連動して割り勘計算処理が走り、結果がリアクティブに画面に表示されるようになった。

リアクティブになった画面

次回はDetailコンポーネントの方の実装を進める。

現時点でのソースコード(App.js)は以下のとおり。

import React, {Component} from 'react';
import './App.css';

// ヘッダーコンポーネント
class Header extends Component {
  render () {
    return (
      <div>
        <div>
          <label>支払額:
            <input type="text"
              value={this.props.payment}
              onChange={e => this.handlePaymentChange(e)} />
          </label>
        </div>
        <div>
          <label>合計額:<span>{this.props.total}</span></label>
        </div>
        <div>
          <label>端数:<span>{this.props.remainder}</span></label>
        </div>
        <div>
          <label>人数:
            <input type="text" value={this.props.participantsNumber} />
          </label>
        </div>
      </div>
    );
  }

  handlePaymentChange (e) {
    const payment = Number(e.target.value);
    if (payment) {
      this.props.onChange(payment);
    }
  }
}

// 明細コンポーネント
class Detail extends Component {
  render () {
    return (
      <div>
        <input type="text" value={this.props.name} />
        <label><input type="radio" value="fix" checked={this.props.kind === 'fix'} />固定</label>
        <label><input type="radio" value="split" checked={this.props.kind === 'split'} />割り勘</label>
        <input type="text" value={this.props.amount} />
      </div>
    );
  }
}

// 割り勘計算機コンポーネント
class BillSplitter extends Component {

  constructor(props) {
    super(props);
    this.state = {
      payment: 0, //支払額
      total: 0, //合計額
      remainder: 0, //端数
      participants: new Array(3).fill().map(() => ({name: '', kind: 'split', amount: 0})), //参加者
    };
    this.handlePaymentChange = this.handlePaymentChange.bind(this);
  }

  handlePaymentChange (payment) {
    // 金額再計算
    const splitAmount = this.calcSplitAmount(payment);
    const participants = this.state.participants.map(p => {
      const amount = p.kind === 'fix' ? p.amount : splitAmount;
      return Object.assign({}, p, {amount: amount});
    });
    const total = participants.map(p => p.amount).reduce((a, x) => a += x, 0);
    const remainder = payment - total;
    // 状態更新
    this.setState({
      payment: payment,
      total: total,
      remainder: remainder,
      participants: participants,
    });
  }

  calcSplitAmount (payment) {
    // (支払額 - 固定支払いの人の合計) / (割り勘支払い人数) => 一人あたり割り勘金額
    const participants = this.state.participants;
    const fixSum = participants.filter(p => p.kind === 'fix').map(p => p.amount).reduce((a, x) => a += x, 0);
    const splitNum = participants.filter(p => p.kind === 'split').length;
    let splitAmount;
    if (payment > fixSum && splitNum) {
      // 100円単位で切り捨て
      const splitAmountTmp = (payment - fixSum) / splitNum;
      splitAmount = Math.floor(splitAmountTmp / 100) * 100;
    } else {
      splitAmount = 0;
    }
    return splitAmount;
  }

  render () {
    const details = this.state.participants.map(person => {
      return <Detail {...person} />
    })
    return (
      <>
        <Header
          payment={this.state.payment}
          total={this.state.total}
          remainder={this.state.remainder}
          participantsNumber={this.state.participants.length}
          onChange={this.handlePaymentChange}
        />
        {details}
      </>
    );
  }

}

function App () {
  return (
    <div className="App">
      <BillSplitter />
    </div>
  );
}

export default App;

コメント

タイトルとURLをコピーしました