React FC ステートで混乱するの巻


Hooksという魔法のせいでなんだかステート（状態）を関数が持てるような錯覚を起こし、
バグで詰まってしまった愚か者がいるらしいですよ。(私のことです)

下記、検証や考察で仮説を建てたものなので誤り等あるかもしれません。
その際はコメントにて指摘いただけると嬉しいです🙇‍♂️

## React FC事件簿
問題が起きていたコードをものすごく簡略化したのが下記のコードになります。

```react

import React, { useEffect, useState } from "react";
import "./styles.css";

export default function App() {
  const [counter, setCounter] = useState(0);
  const [madeDom, setMadeDom] = useState(null);

  const counterCheck = () => {
    alert(`カウントは${counter}`);
  };

  useEffect(() => {
    setMadeDom(<ChildComp handler={counterCheck} />);
  }, []);

  return (
    <div className="App">
      現在カウンターは {counter}
      <button onClick={() => setCounter(counter + 1)}>カウントUP</button>
      {madeDom}
    </div>
  );
}

function ChildComp({ handler }) {
  return <button onClick={handler}>子供カウントチェック</button>;
}


```
[CodeSandBoxはこちら](https://codesandbox.io/s/hooks-stateless-j8nef?file=/src/App.js)


親コンポーネントは初回レンダリングの際に `useEffect`にて`madeDom`を作成し、それを
`useState`で保持しています。
またmadeDomには、親コンポーネントのメソッド `counterCheck`を渡しています。
こちらは親のステート `counter` を表示するメソッドです。

親コンポーネントはビューとして
・自身のカウンター、
・カウントアップ用ボタン
・生成したmadeDom
を表示させています。

![スクリーンショット 2020-12-16 20.31.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/441432/bb7d4f3b-94fb-e171-a0f2-6bcc2e704bd8.png)

初回はこんな塩梅ですね。
そしてカウントUPさせたのち、子供カウントチェックボタンを押すと....

![スクリーンショット 2020-12-16 20.34.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/441432/cef9b45d-b9cc-59b0-979d-61fc973a19a7.png)


あれ、、、
**同じcounterを保持しているはずなのに親と子で異なる結果となりました。**
もっと正確にいうなら、子に渡したメソッドから参照する`counter`は増加せず、
親からダイレクトに表示している`counter`のみのカウントが増加しています。
ここで理由が即座に説明できる方には、もしかしたらこの記事を読む必要はないかもしれません。
ですが、私と同じような混乱を感じている方は引き続きお付き合いいただけたらと思います。


## Classは状態がありますので
上記と全く同じ実装をClassでやってみましょう。


```react

import React, { Component } from "react";
import "./styles.css";

export default class App extends Component {
  constructor(props) {
    super(props);
    this.countCheckHandler = this.countCheckHandler.bind(this);
    this.state = {
      counter: 0,
      madeDom: null
    };
  }

  countCheckHandler() {
    alert(`カウントは${this.state.counter}`);
  }
  componentDidMount() {
    this.setState({
      madeDom: <ChildComp handler={this.countCheckHandler} />
    });
  }
  render() {
    return (
      <div className="App">
        現在カウンターは {this.state.counter}
        <button
          onClick={() => this.setState({ counter: this.state.counter + 1 })}
        >
          カウントUP
        </button>
        {this.state.madeDom}
      </div>
    );
  }
}

class ChildComp extends Component {
  render() {
    return (
      <button
        onClick={() => {
          this.props.handler();
        }}
      >
        子供カウントチェック
      </button>
    );
  }
}

```

[CodeSandBoxはこちら](https://codesandbox.io/s/class-stateful-220nl?file=/src/App.js:0-958)

![スクリーンショット 2020-12-17 18.19.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/441432/7f571255-d596-0b76-e774-0c1915b36663.png)


クラスに変えただけなのですが、子コンポーネント経由で増加分を正しく表示できています。
繰り返しになりますが、クラスは内部情報を持ちますが関数は持ちません。
少し真相に近づいてきました。

### classコンポーネントが見ているデータ先
実際に`this`で確認してみましょう。 渡しているメソッドにconsoleを付け加えます。

```diff

  countCheckHandler() {
+    console.log(this);
    alert(`カウントは${this.state.counter}`);
  }
```
![スクリーンショット 2020-12-17 18.23.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/441432/c1793652-0e54-3333-3265-1d37dfedbc08.png)
意図していた通り親コンポーネントの `App`が参照されています。
なので、ハンドラー内で呼び出している `this.state.counter` は間違いなく **Appの**counterが呼び出されています。

【Classでの内部状態考察】

![class.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/441432/dc31f4c3-447b-02b9-47f2-fd8a2e979daf.png)


では関数コンポーネントではどうでしょうか。
【Funcでの内部状態考察】
![func.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/441432/10591e2a-91b3-6fca-c186-c5a64dfd06e1.png)

このように、生成時のみCounterをコピーしてくる形になるので、その後Appの情報が増えてもChildCompは知らんべ、ということのようです。
なぜなら、関数は内部状態を持たないので（本日n回目）、Appのcounter状態をみる、という芸当はChildComp側はできないわけです。

じゃあuseStateってなんなんだ! propsってなんなんだ! 状態みたいなの保ててるんだけど!?!?!?
と混乱したところで、そもそもHooksで表現しているStateの仕組みって何よ、ってところをおさらいします。

## そもそもHooksってどうやって状態を表現しているんだろう。

https://daveceddia.com/intro-to-hooks/#the-magic-of-hooks
上記をぜひ読んでください。
...だけでは味気ないので、ゆる〜く超意訳してみます。

Reactがfunctionコンポーネントを初めてレンダリングする際、オブジェクトを生成します。
このコンポーネントのオブジェクトはDOMに存在し続ける限りずっと生き続けます。
Reactはこのオブジェクトを使用して、色々なメタデータを扱っているわけですね。

また、コンポーネントは自分でレンダリングするのではなく、Reactが呼び出すことでレンダリングされます。
コンポーネント自体は返すものは、DOMノードに変換可能なオブジェクト構造でしかありません。

このReactが呼び出すための準備の際にstateがセットアップされます。

```react
function AudioPlayer() {
  const [volume, setVolume] = useState(80);
  const [position, setPosition] = useState(0);
  const [isPlaying, setPlaying] = useState(false);

}
```
(コードは記事からそのまま拝借しました)
このように3回`useState`が呼び出された場合、Reactは3つの値を配列に入れていきます。
次にレンダーされる場合、この3つのhooksは常に同じ順番で呼び出されます（呼び出し順は常に同じでなくてはならない、というhooksのルールを思い出してください。）そして、新しい状態を作る代わりに、2回目のレンダーではそのポジションにある値を返します。

これがReactが変数がスコープ外の複数の関数の呼び出しがあってもステートを作成・維持できる方法です。
単にオブジェクトであるというのがミソですね。


## hooksで起きた問題箇所を詳しく調べる
少しコードサンプルを変えて色々検証してみます。

```react
import React, { useEffect, useState } from "react";
import "./styles.css";

export default function App() {
  const [count, setCount] = useState(0);
  // １つ目は変数に格納したものを表示させる
  const myChild = <Child count={count} />;
 
 //２つ目は初回レンダリング次のみ生成し、それを保持する
  const [myChild2, setMyChild2] = useState(null);
  useEffect(() => {
    setMyChild2(<Child count={count} />);
  }, []);
  return (
    <div className="App">
      <button
        onClick={() => {
          setCount(count + 1);
        }}
      >
        UP
      </button>
      <Child count={count} />
      {myChild}
      {myChild2}
    </div>
  );
}

function Child({ count }) {
  return (
    <>
      <p>カウントは{count}</p>
      <button
        onClick={() => {
          alert(count);
        }}
      >
        カウントチェック
      </button>
    </>
  );
}
```

[CodeSandBoxはこちら](https://codesandbox.io/s/kind-johnson-944z1?file=/src/App.js:0-752)

わざわざ子コンポーネントに確認用alertのハンドラーを追加しているのは、
レンダリングはされていないが内部の情報は更新されているかも？という疑いを検証するためです。

![スクリーンショット 2020-12-18 15.20.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/441432/73e6276d-7141-000d-d2c7-afe7a283c2c2.png)

このように`useEffect`で初回レンダリングで生成している`Child`コンポーネントのみ、
親のcountを追えていないことがわかります。
もちろん、アラートでの表示も同様でした。

つまりuseEffectで初回のみ`myChild2`を再計算させているため、
useStateで生成されたオブジェクトが追えていない、ということのようです。
本来であれば
・`count`が変わる
・App内が再計算される
・Childコンポーネントのprops　も再計算される
という流れがうまく働いていなかったことが原因でした。

## まとめと書簡
今後useEffect内でコンポーネントを生成する場合、
再計算されないこと/そして関数である故に、propsの値などを直に参照できていると思い込まないことに注意していこうと思います。

また、検証に当たってreact内の実装をガツガツ読めるようになった方がより検証しやすいな〜と思ったので、
もっと実装を直でガツガツ読めるようになりたいです..(途中までコード追ってたのですが挫けました)

## 改善しました

```react

import React, { useState } from "react";
import "./styles.css";

export default function App() {
  const [counter, setCounter] = useState(0);

  const counterCheck = () => {
    alert(`カウントは${counter}`);
  };

  return (
    <div className="App">
      現在カウンターは {counter}
      <button onClick={() => setCounter(counter + 1)}>カウントUP</button>
      <ChildComp handler={counterCheck} />
    </div>
  );
}

function ChildComp({ handler }) {
  return <button onClick={handler}>子供カウントチェック</button>;
}

```
[codeSandBoxはこちら](https://codesandbox.io/s/state-ok-89qct?file=/src/App.js:0-496)

実際はもっと複雑だったのですが、上記のような形で
レンダリングにChildCompを書き込むことで、問題なくカウンターを呼び出すことができるようになりました。

または、useEffectの第二引数に依存する変数を指定することでも改善できます。

```react
import React, { useEffect, useState } from "react";
import "./styles.css";

export default function App() {
  const [counter, setCounter] = useState(0);
  const [madeDom, setMadeDom] = useState(null);

  const counterCheck = () => {
    alert(`カウントは${counter}`);
  };

  useEffect(() => {
    setMadeDom(<ChildComp handler={counterCheck} />);
  }, [counter]);

  return (
    <div className="App">
      現在カウンターは {counter}
      <button onClick={() => setCounter(counter + 1)}>カウントUP</button>
      {madeDom}
    </div>
  );
}

function ChildComp({ handler }) {
  return <button onClick={handler}>子供カウントチェック</button>;
}

```

めでたしめでたし。