計算苦手な妻「構成比？前年比？伸び率？」←Webサービス作って解決してみた【React】


## 妻は計算ができない

ワイ (｀･ω･´) 「

こんにちは。masakichiです。

あなたの身近に計算が苦手なひとはいませんか？

ワイの妻は仕事につかう計算が苦手で、構成比やら前年比やら伸び率やらを出さないといけない時、ワイにヘルプを求めてきます。

たまにならいいのですが、ほぼ毎回ヘルプミーしてくるのです。

そんなわけで、いっそのことWebサービス作って、そっちで計算してもらおうと思ったわけです。

」

## つくったもの

![image](https://raw.githubusercontent.com/Naughty1029/React-Calculator/main/explanation.gif)

URL
https://react-calculator.cocoroworks.net/

ワイ (*´ `) 「仕事でよく使う計算が簡単にできるWebサービスをつくりました！」

計算苦手な人 ( ´･ω･)　「あれ？この計算ってどうやって求めればよかったんだっけ？」

↑これを解決することができます。


### 仕事でよく使う10種類の計算ができます
ワイ (*´ `) 「一般的に仕事でよく使いそうな10種類の計算に対応してみました」

ワイ (*´ `) 「具体的には以下の計算です」

- 構成比
- 前年比の売上
- 目標達成率
- 伸び率
- 受講率
- 定価
- 原価
- 利益率
- 原価率
- リピート率

### 妻「グラフの表示もしてや。画像でダウンロードしたい。」
エクセルでグラフを作るのも苦手な妻からの要望。

ワイ　(´･ω･`)ｏ０* (

ぐぬぬ。そういえば、グラフ入りの資料作る際はワイがいつも作ってあげてたな…
妻の会社の資料なんだけどな…
なんだったら、妻は資料（ワイの作ったやつぅ）作るの上手って思われてるみたいで、よく資料作成頼まれるらしいし…
ワイにも少しお給料くれんかな？

)

妻　**(*´꒳`*)「作れるよね？（圧っ）」**

ワイ　**（；´д｀ )「もちろんですとも！任せい！」**

ということで、計算結果がチャート化されて、png画像でダウンロードすることも可能です。

## 技術的なこと
ここからは技術的な内容に触れていこうと思います。

## 開発環境
開発環境は以下の通りです。
React / TypeScriptの学習も兼ねているので、Recoilも導入してみました。
スタイリングはMaterial UIに委ねています。

```
react: 17.0.2
typescript: 4.7.2
recoil: 0.7.3
recharts: 2.1.10
recharts-to-png: 2.1.0
file-saver: 2.0.5
emotion/react: 11.9.0
mui/material: 5.8.2
```

### コンポーネントとディレクトリ構成について
コンポーネントは以下のように大きく３つのコンポーネントに分割しました。

![components.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1006362/ced09318-0d54-0293-10a4-080f538972c5.jpeg)


ディレクトリ構成は下記リポジトリを参考にしつつ、単一責任の原則とネストの深さに注意して構築しました。（といいつつも、わたしは未熟なので中身はズタボロです。）

https://github.com/alan2207/bulletproof-react

```text:ディレクトリ構成
src/
├ assets/　　- CSSファイルなど
├ components/　- コンポーネント
|    └ Chart/
|    └ Formula/
|    └ Head/
|    └ Layout/　- レイアウト表示用
├ hooks/　　- カスタムフック
├ store/　　- Recoil使用
├ types/
└ utils/　　- 計算結果を返却する関数定義
```

### Recoilを使ってみる
今回作ったWebサービスでは以下のように３つのコンポーネント間でstateを共有し合います。

そのため、普通にやるとstateのバケツリレーが生じてしまうので、状態管理ライブラリを使うことにしました。

![chart.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1006362/3745621f-b84a-d1ce-9ef2-1878d5faf10b.jpeg)

### 計算する仕組みについて
計算式のデータはjsonで管理しています。

```json:formulas.json
[
    {
      "id": 1,
      "title": "構成比",
      "item":["全体","一部"],
      "calc":"kouseihi",
      "rate": true,
      "chart": "pie"
    },
    ...
]
```

上記jsonデータから、現在選択されている計算式のデータを管理するstateを準備します。

```react:src/store/formulaState.ts
import { atom } from 'recoil';
import Data from '../formulas.json';
import { FormulaType } from '../types/Formula/FormulaType';

export const formulaState = atom<FormulaType>({
  key: 'formulaState',
  default: Data[0],
});
```

次に、計算結果を返却するための関数群をオブジェクトして定義しました。
上記計算式のデータのcalcキーの値と関数名がそれぞれ対応する形にしています。

また、渡されてくるユーザー入力値の個数が計算式によって変わる可能性を考慮し、関数の引数をスプレッド構文にしたことも工夫点の１つです。

```react:src/utils/CalcFunctions.tsx
//計算式を定義するためのファイル
export const CalcFunctions = {
  kouseihi: (...nums: Array<number>): number => {
    const calc = nums[1] / nums[0];
    return calc;
  },
  ...
}
```

ユーザーの入力値は配列で管理します。
こうすることで先程の計算結果を返却する関数（CalcFunctions）に対して、スプレッド構文で引数を渡すことができます。

```react:src/store/inputNumberArrayState.ts
import { atom } from 'recoil';

export const inputNumberArrayState = atom<Array<number>>({
  key: 'inputNumberArrayState',
  default: [],
});

```

以下が計算結果を返却する関数（CalcFunctions）を実行するためのトリガーです。

handleCalculateの引数にはformulas.jsonの中で、現在アクティブになっている計算データ（formula）のcalcキーの値とユーザー入力値を格納した配列を渡しています。

先程のCalcFunctionsを呼び出し、計算結果をresultとしてステート管理します。

```react:src/
import { CalcFunctions } from '../../utils/CalcFunctions';

const handleCalculate = (calc: string, inputNumberArray: Array<number>): void => {
    const typeCalc: keyof FunctionsType = calc as keyof FunctionsType;
    const func = CalcFunctions[typeCalc];
    let result = func(...inputNumberArray);
//省略
}

return (
      <Button
        variant="contained"
        onClick={() => handleCalculate(formula['calc'], inputNumberArray)}
        size={'large'}
        sx={{ mt: 4, mb: 4 }}
        style={{ fontSize: '20px' }}
      >
        計算する
      </Button>
)
```

### チャート表示ライブラリについて
Rechartsを使用しています。

画像ダウンロード機能もプラグインを利用すれば実装可能でした。

こちらの記事を参考にライブラリ選定しました。

https://qiita.com/niranken/items/dbaa17e7e56f4ac0dd77

#### ダウンロードボタンについて
BarチャートとPieチャートを表示するための、コンポーネントを２つ作ったのですが、画像ダウンロードボタンを共通化したかったので、下記ブログを参考にrender hooksを作りました。

https://engineering.linecorp.com/ja/blog/line-securities-frontend-3/

実際には、以下のhooksを作りました。

```react:src/hooks/useDownloadButton.tsx
import FileSaver from 'file-saver';
import { useCallback } from 'react';
import { useCurrentPng } from 'recharts-to-png';
import { Button } from '@mui/material';

export const useDownloadButton = () => {
  const [getPng, { ref, isLoading }] = useCurrentPng();
  const handleDownload = useCallback(async () => {
    const png = await getPng();
    if (png) {
      FileSaver.saveAs(png, 'myChart.png');
    }
  }, [getPng]);
  const renderButton = () => (
    <Button
      onClick={handleDownload}
      variant="contained"
      size="large"
      sx={{ display: 'block', marginLeft: 'auto', marginTop: '10px' }}
    >
      {isLoading ? 'ダウンロード中...' : '図をダウンロード'}
    </Button>
  );
  return { renderButton, ref };
};
```

それぞれチャートを出力するコンポーネントで呼び出して使います。

下記はBarChartを出力するコンポーネントで呼び出しています。
※コンポーネントの内容は割愛

```react:src/components/Chart/ShowBarChart.tsx
import { memo, useEffect, VFC } from 'react';
import { useDownloadButton } from '../../hooks/useDownloadButton';

//省略

export const ShowBarChart: VFC<Props> = memo(({ formula, result }) => {
  const { renderButton, ref } = useDownloadButton();
 //省略 
 return (
    <>
      {//省略}
      <BarChart data={chartData} ref={ref}>
        {//省略}
        <Bar dataKey="num" fill="#82ca9d" label={renderCustomBarLabel} />
      </BarChart>
      {renderButton()}
    </>
  );
});

```

## おわりに
これを作ってあげたところ、妻は大喜び。
無事に計算式をわたしに聞いてくる回数は激減しました。

やっぱり、何かを解決するというのが原点にあると、学習も捗るし、モチベーションの継続にもつながるなと改めて感じました。

ただチャートのデザインなどはどうやら仕事で使えそうなクオリティには到達していないようで、また近々何か作ってと要望を頂きそうです…

## 各リンク先
- 仕事でよく使う計算（本サービス）


https://react-calculator.cocoroworks.net/

- githubリポジトリ


https://github.com/Naughty1029/React-Calculator

修正点・コードのリファクタリングなど、どんなissue/prでも受付しています！