React公式ドキュメント「state のリフトアップ」をFunctional Componentに書き換える

# この記事について

Reactには__Class Component__と__Funcional Component__の2種類が存在しています。

Reactの公式ドキュメント「__state のリフトアップ__」はClass Componentで書かれているので、これをFuncional Componentに書き換えていきたいと思います。

https://ja.reactjs.org/docs/lifting-state-up.html

> この章では、与えられた温度で水が沸騰するかどうかを計算する温度計算ソフトを作成します。

まだまだ手探りで学習中の身ですので、より良い書き方がある場合などあればぜひご教示ください。

## 実行環境

- react: 17.0.2
- react-dom: 17.0.2
- react-scripts: 4.0.3

# 開始

`BoilingVerdict`と`Calculator`のみが存在する状態から開始します。

```diff:BoilingVerdict
- function BoilingVerdict(props) {
+ const BoilingVerdict = (props) => {
    if (props.celsius >= 100) {
      return <p>The water would boil.</p>;
    }
    return <p>The water would not boil.</p>;
  }
```

`BoilingVerdict`は初めからfunctionalなため書き換える必要は無いのですが、アロー関数での記法に統一したいため書き換えを行います。

```diff:Calculatorの宣言
- class Calculator extends React.Component {
+ const Calculator = () => {
```

```diff:Calculatorのconstructor
-   constructor(props) {
-     super(props);
-     this.handleChange = this.handleChange.bind(this);
-     this.state = { temperature: "" };
-   }
-   handleChange(e) {
-     this.setState({ temperature: e.target.value });
-   }
+   const [temperature, setTemperature] = useState("");
+   const handleChange = (e) => setTemperature(() => e.target.temperature);
```

```diff:Calculatorのrender
-   render() {
-     const temperature = this.state.temperature;
      return (
        <fieldset>
          <legend>Enter temperature in Celsius:</legend>
-         <input value={temperature} onChange={this.handleChange} />
+         <input value={temperature} onChange={handleChange} />
          <BoilingVerdict celsius={parseFloat(temperature)} />
        </fieldset>
      );
-   }
  }
```

stateはHookを使うことでかなり簡潔に書けるようになります。Fnctionalに書き換えることで参照時に`this`を使用する必要がなくなります。なおHookは`const [state要素名, state更新用関数] = useState(state初期値);`となっています。

`handleChange`についても`bind`が不要となりました。

# 2 つ目の入力を追加する

この章では`Calculator`を`Calculator`と`TemperatureInput`に分離しています。

```diff:scaleNames
+  const scaleNames = {
+    c: "Celsius",
+    f: "Fahrenheit",
+  };
```

```diff:TemperatureInput
+ const TemperatureInput = (props) => {
+   const [temperature, setTemperature] = useState("");
+   const handleChange = (e) => setTemperature(() => e.target.value);
+   const scale = props.scale;
+   return (
+     <fieldset>
+       <legend>Enter temperature in {scaleNames[scale]}:</legend>
+       <input value={temperature} onChange={handleChange} />
+     </fieldset>
+   );
+ }
```

```diff:Calculator
  const Calculator = () => {
-   const [temperature, setTemperature] = useState("");
-   const handleChange = (e) => setTemperature(() => e.target.value);
    return (
-     <fieldset>
-       <legend>Enter temperature in Celsius:</legend>
-       <input value={temperature} onChange={handleChange} />
-       <BoilingVerdict celsius={parseFloat(temperature)} />
-     </fieldset>
+     <div>
+       <TemperatureInput scale="c" />
+       <TemperatureInput scale="f" />
        <BoilingVerdict celsius={parseFloat(temperature)} />
+     </div>
    );
  }
```

`const TemperatureInput = (props) => {`は`const TemperatureInput = ({ scale }) => {`でも大丈夫です。

# 変換関数の作成

3つの関数を追加します。ドキュメントと異なる点はアロー関数に変更した点のみです。

```react
const toCelsius = (fahrenheit) => {
  return ((fahrenheit - 32) * 5) / 9;
};

const toFahrenheit = (celsius) => {
  return (celsius * 9) / 5 + 32;
};

const tryConvert = (temperature, convert) => {
  const input = parseFloat(temperature);
  if (Number.isNaN(input)) {
    return "";
  }
  const output = convert(input);
  const rounded = Math.round(output * 1000) / 1000;
  return rounded.toString();
};
```

# state のリフトアップ

ドキュメントでも触れている通り、現在のコードでは2つの`TemperatureInput`は独立した`temperature`を保持しており同期されていません。これを同期させます。

```diff:TemperatureInput
  const TemperatureInput = (props) => {
-   const [temperature, setTemperature] = useState("");
+   const temperature = props.temperature;
-   const handleChange = (e) => setTemperature(() => e.target.value);
+   const handleChange = (e) => props.onTemperatureChange(() => e.target.value);
    const scale = props.scale;
    return (
      <fieldset>
        <legend>Enter temperature in {scaleNames[scale]}:</legend>
        <input value={temperature} onChange={handleChange} />
      </fieldset>
    );
 }
```

`TemperatureInput`でstateを管理するのをやめて`Calculator`で管理させるため、`temperature`をstateからpropsに変更します。`handleChange`もpropsの`onTemperatureChange`を使用するよう変更します。

```diff:Calculator
  const Calculator = () => {
+   const [temperature, setTemperature] = useState("");
+   const [scale, setScale] = useState("c");
+   const handleCelsiusChange = (temperature) => {
+     setTemperature(temperature);
+     setScale("c");
+   };
+   const handleFahrenheitChange = (temperature) => {
+     setTemperature(temperature);
+     setScale("f");
+   };
+   const celsius = scale === "f" ? tryConvert(temperature, toCelsius) : temperature;
+   const fahrenheit = scale === "c" ? tryConvert(temperature, toFahrenheit) : temperature;
    return (
      <div>
-       <TemperatureInput scale="c" />
+       <TemperatureInput
+         scale="c"
+         temperature={celsius}
+         onTemperatureChange={handleCelsiusChange}
+       />
-       <TemperatureInput scale="f" />
+       <TemperatureInput
+         scale="f"
+         temperature={fahrenheit}
+         onTemperatureChange={handleFahrenheitChange}
+       />
        <BoilingVerdict celsius={parseFloat(temperature)} />
      </div>
    );
  }
```

`TemperatureInput`のstateを削除したので親要素である`Calculator`にstateを追加します。渡すpropsとして`temperature`と`onTemperatureChange`を追加します。

# まとめ

`setState`というHookの登場によりFunctional Componentでもstateを管理できるようになったということを、実際に書き換えを行うことで学ぶことが出来ました。

実は今回は途中で妥協してClass Componentでもいいや、と投げ出しそうな予感がしたのでQiita記事を並行で書きながら試行錯誤する作戦で取り組んでみたのですが、効果抜群でコンコルド効果にも捕らわれたおかげで投げ出せなくなったのでオススメです。

## 完成コード

GitHubに完成コードをプッシュしています。

https://github.com/big-mon/react-lifting-function/blob/main/src/index.js

## 参考にした記事

- [[備忘録]Reactの関数コンポーネントでStateを使う方法](https://cpoint-lab.co.jp/article/202008/16459/)
- [Reactでクラスコンポーネントより関数コンポーネントを使うべき理由5選](https://tyotto-good.com/blog/reaseons-to-use-function-component)
- [React hooksを基礎から理解する (useState編)](https://qiita.com/seira/items/f063e262b1d57d7e78b4)