JavaScript
Vue.js
hooks
React
redux

Reactにおける算出プロパティの書き方


目次


算出プロパティとは?

算出プロパティとはvue.jsで利用できる

コンポーネントで利用されるプロパティがある別のプロパティから一意に決まる場合にその値をstateに保存せずに都度算出させるプロパティ

のことです。

言葉の定義で説明すると非常に分かりづらいので簡単な例を考えてみます。


算出プロパティ(税込金額)の例

例えば、ユーザーに金額を入力させて、入力した税込金額を同時に計算して表示する画面を作りたいとします。

このとき以下のようにユーザーが入力した金額と計算した税込金額をstateに保存して両方表示する実装をしたとしましょう。(※ 概念的な実装)

...

const state = {
price: price,
priceWithTax: priceWithTax
}
...
<ul>
<li>{price}</li>
<li>(税込){priceWithTax}</li>
</ul>

上記の実装には1つ大きな問題があります。

それは

:warning: priceWithTaxのstateに誤った金額がセットされた場合、税込金額がずれる可能性がある。 :warning:

です。

いくらロジック的に正しかったとしても別のところでstateをsetされたらずれてしまい、税込金額が必ず正しいことを保証できない状態になってしまっています。

なぜこんなことが起こるのかというと税込金額をstateに含んでしまっているからですね。

そもそも税込金額 は

税込金額 = 金額 x 税率

で計算できるのでstateに含めるべきものではありません。

今回の場合、定義した算出プロパティと照らし合わせると

コンポーネントで利用されるプロパティ(= 税込金額)がある別のプロパティ(= 金額)から一意に決まる場合にその値をstateに保存せずに都度算出させるプロパティ

となるわけですね。

なので

以下のように都度メソッドで算出することで絶対にずれないことを保証することができます。

...

const state = {
price: price,
}
...
// 算出プロパティを計算するメソッド
const withTax = (price) => price * 1.08
...
<ul>
<li>{price}</li>
// 算出プロパティ
<li>(税込){withTax(price)}</li>
</ul>

ではReactで算出プロパティを実装したい場合はどのように書けばいいのでしょうか?

この記事ではReactにおける算出プロパティの書き方を3つご紹介します。

なお今回使用したコードはこちらからいじれるので実際動かして試してみてください。


Reactにおける算出プロパティの書き方


1. 愚直にメソッドで計算する

引き続き、金額から税込の金額を表示する単純なコンポーネントについて考えていきたいと思います。

まず一番シンプルな方法として愚直にメソッドで書くという方法があり、

コードは以下のようになります。

こちらから展開できます。


import React, { Component } from "react";

const TAX_RATE = 1.08;
class TaxCaliculator extends Component {
constructor(props) {
super(props);
this.state = {
rawPrice: 0,
toggle: true
};
}

withTax(rawPrice) {
console.log(`Legacy: calc tax ${rawPrice}`);
return rawPrice * TAX_RATE;
}

render() {
const { rawPrice, toggle } = this.state;
return (
<ul>
<li>税抜き: {rawPrice} </li>
<li>税込み: {this.withTax(rawPrice)} </li>
<button onClick={() => this.setState({ rawPrice: rawPrice + 100 })}>
+
</button>
<button onClick={() => this.setState({ toggle: !toggle })}>
{String(toggle)}
</button>
</ul>
);
}
}
export default TaxCaliculator;


ポイントはこの部分でwithTaxメソッドを使って税抜き金額から税込金額を計算しています。


// 税込金額を計算して返すメソッド
withTax(rawPrice) {
return rawPrice * TAX_RATE;
}
...
// 算出プロパティ
<li>税込み: {this.withTax(rawPrice)} </li>

シンプルでわかりやすい書き方で小さい簡単なアプリケーションを作る場合は良いですが、アプリケーションが大きくなってくると


  • コンポーネントにたくさん算出プロパティのためのメソッドができてしまい、コンポーネントが大きくなり読みづらくなる。

  • 共通処理になったときにutil的なものに書きがちで負債になりやすい

  • 関係無いstateが変更される度に再計算される。

などデメリットが大きくなってきます。

特に3つ目のデメリットは特に大きく、再計算の必要が無いのに毎度計算されてしまい、重い処理だとパフォーマンス的にも問題が出てきてしまいます。


2. reselect

そこで登場するのがreselectです。

reselectはreduxで算出プロパティを扱うためのライブラリで、selectorと呼ばれる算出プロパティを計算するためのオブジェクトを定義して利用します。

実際にreselectを使って同じ消費税計算のコンポーネントを書いたのが以下です。

(reduxは今回は利用していません。)

こちらから展開できます。


// selectorを定義
const TAX_RATE = 1.08;
const priceSelector = state => state.rawPrice;
const taxSelector = createSelector(
priceSelector,
price => {
console.log(`Reselect: calc tax ${price}`);
return price * TAX_RATE; // 算出プロパティを計算
}
);

class WithReselect extends Component {
constructor(props) {
super(props);
this.state = {
rawPrice: 0,
toggle: true
};
}

render() {
const { rawPrice, toggle } = this.state;
return (
<ul>
<li>税抜き: {rawPrice} </li>
// selectorから算出プロパティを取得
<li>税込み: {taxSelector(this.state)} </li>
<button onClick={() => this.setState({ rawPrice: rawPrice + 100 })}>
+
</button>
<button onClick={() => this.setState({ toggle: !toggle })}>
{String(toggle)}
</button>
</ul>
);
}
}

export default WithReselect;


ポイントはselectorを作っている以下の箇所で


  • priceSelector: stateを受け取りpriceを返す

  • taxSelector: priceを受け税込の値段を返す

2つのセレクターを定義し、コンポーネントで利用することで税込金額を取得しています。

// slectorを定義

const TAX_RATE = 1.08;
const priceSelector = state => state.rawPrice;
const taxSelector = createSelector(
priceSelector,
price => {
console.log(`Reselect: calc tax ${price}`);
return price * TAX_RATE;
}
);

...
// selectorから算出プロパティを取得
<li>税込み: {taxSelector(this.state)} </li>


reselectを使うメリット


storeを最小限に保つことができる & 不整合が生じ得ない

今回はreduxを使用していませんが、実際には以下のようにredux connectなどと組み合わせて使い、これまで計算後の値もstoreに保存してしまいがちだったのが保存する必要がなくなり、storeに保存するデータを最小限に保つことができるかつ不整合が生じ得なくなります。

const mapStateToProps = (state) => {

return {
todos: getVisibleTodos(state)
}
}
...
const VisibleTodoList = connect(
mapStateToProps,
mapDispatchToProps
)(TodoList)


selectorは引数が変更されない限り再計算されない

各selectorは前回の計算結果をキャッシュしているので引数が変更されない限り再計算されず、キャッシュした値を返します。このようなキャッシュの仕組みをmemoizeと呼びます。

今回のソースコード+の横に別の無関係なstateをtoggleするようにしていてlegacyでは都度計算が再実行されるのに対してreselectを使った場合は金額が変化しない限り再計算されていないことがわかります。

愚直にメソッドで実装の場合、関係ないstateが変更されても再計算される

ezgif.com-gif-maker.gif

reselectの場合、selectorの引数(金額)が変更されない限り再計算されない(memoizeされている)

ezgif.com-gif-maker (1).gif


selectorは他のselectorのinputとして利用できる

例えば金額から30%割引するselectorを作成したい場合先ほどのpriceSelectorを使いまわすことができます。

const priceSelector = state => state.rawPrice;

cosnt taxSelector = ...
// priceSelectorをinputとして再利用
const discountSelector = createSelector(
priceSelector,
price => price * 0.7
);


3. ReactHooksのuseMemo

useMemoはReact 16.8から導入されたReactHooksの中の1つで、算出プロパティを簡単に書くことができます。

実際のuseMemoを使って今回の消費税計算のコンポーネントを書いたのが以下です。

こちらから展開できます。


import React, { useState, useMemo } from "react";

const Hooks = () => {
const [rawPrice, setPrice] = useState(0);
 // useMemoで算出プロパティを定義(rawPriceが変更されたら再計算される)
const withTaxPrice = useMemo(() => rawPrice * 1.08, [rawPrice]);
const [toggle, setToggle] = useState(true);
return (
<ul>
<li>税抜き: {rawPrice} </li>
<li>税込み: {withTaxPrice} </li>
<button onClick={() => setPrice(rawPrice + 100)}>+</button>
<button onClick={() => setToggle(!toggle)}>{String(toggle)}</button>
</ul>
);
};
export default Hooks;


ポイントはuseMemoを使っている箇所でuseMemoは以下のように書くことができます。

第二引数のdependencyには算出プロパティに依存する値、つまりこの値が更新されたら再計算してほしい値をセットします。

useMemo({算出プロパティを計算する関数}, [dependency]})

今回の例の場合は以下のように書いており、rawPriceが変更されたときのみ第一引数の関数が再計算されます。

const withTaxPrice = useMemo(() => rawPrice * 1.08, [rawPrice]);

ここでの説明は省略しますがreselectであげたようなメリットはuseMemoでも一通り享受することができます :thumbsup:


まとめ

Reactにおける算出プロパティの書き方についてまとめると


  • Hooksが使えるならuseMemoを使う

  • 使えない場合


    • 小さい単純なアプリ → 愚直にメソッドで書く

    • Reduxを使うような大きいアプリの場合 → reselectを使う



といったところでしょうか。

useMemoについてはこちらの記事でさらに詳しくかいてあるので参考にしてみてください。

(※ 本当にパフォーマンスをめちゃくちゃ気にしないといけない環境ならこの記事の通りコストを考えて、メソッドで計算するとuseMemoを使うを使い分けないかもしれません。大半の場合は統一されていなくてわかりにくくなるデメリットの方が大きいかなと自分は考えています。)

どなたかの参考になれば幸いです :thumbsup:

似たようなHOCの記事もこちらにあるのでよければご覧ください!

なぜReactHooksを使うとrecomposeが不要になるのか?

参考:

reselect

ReactHooks

雰囲気で使わない React hooks の useCallback/useMemo