親コンポーネントがステートを持つべきなの？それとも子コンポーネント？

**※7割くらい ChatGPT で書いた記事です。**

# ある日の我が家

娘「ねえ、パパ」

ワイ「なんや、娘ちゃん？」

娘「コンポーネントには、状態っていう概念があるじゃない？」

ワイ「あるなぁ」

娘「親コンポーネントが状態を持つべきなの？」
娘「それとも子が持つべきなの？」
娘「どう決めるべきか分からないんだよね」

ワイ「おお、ええ質問やな」
ワイ「具体例を見ながら話を進めてみよか」

娘「うん！」
娘「娘、具体例、好き！」

ワイ「まずは、**トグルスイッチ**のコンポーネントを例に考えてみるで」

# 子コンポーネントに状態を持たせる場合

ワイ「コードはこんな感じや」

```tsx
// トグルスイッチのコンポーネント
const ToggleSwitch: React.FC = () => {
  // オン・オフの状態を管理
  const [isOn, setIsOn] = useState(false)

  // オン・オフをクリックした時に発火する関数
  const onClick = () => {
    // オン・オフの状態を反転させる
    setIsOn(!isOn)
  }

  return (
    <button type="button" onClick={onClick}>
      {isOn ? "ON" : "OFF"}
    </button>
  )
}
```

娘「オン・オフの状態を`ToggleSwitch`自体に持たせるってことだね？」

ワイ「せや」
ワイ「そんで、ページ側から`ToggleSwitch`を呼び出してやるんや」


### ページ側

```tsx
const SamplePage = () => {
  return (
    <form>
      <label>
        <span>設定内容1</span>
        <ToggleSwitch />
      </label>
      <label>
        <span>設定内容2</span>
        <ToggleSwitch />
      </label>
      <label>
        <span>設定内容3</span>
        <ToggleSwitch />
      </label>
    </form>
  )
}
```

### 画面はこんな感じ

![スクリーンショット 2023-06-29 11.52.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/295452/0adfa955-1611-f4ff-8a7f-cf5bec0dce5b.png)


娘「なるほどね」
娘「これは・・・よくない気がする！」
娘「子コンポーネントに状態を持たせるより」
娘「ページ側に持たせたほうが良さそうな気がしてきた！」

ワイ「お、そうやな」
ワイ「複数の`ToggleSwitch`があって、**全ての状態を知りたいとき**に不便やもんな」

娘「そうそう、全てのトグルスイッチの状態を、まとめてフォームで送信したいときとか」
娘「ページ側で状態を管理してないと困っちゃいそう」

ワイ「せやな」
ワイ「ほな、ちょいとコードを修正して」
ワイ「ページ側に状態を持たせてみるでぇ」

# ページ側に状態を持たせる場合

ワイ「ページ側で、`useState()`を使って状態管理してやるんや」

```diff_tsx
  const SamplePage = () => {
    // ページ側で状態を保つ
+   const [formState, setFormState] = useState({
+     state1: false,
+     state2: false,
+     state3: false,
+   })

    // 状態を更新するためのヘルパー関数
+   const switchState = (key: keyof typeof formState, bool: boolean) => {
+     /* 省略 */
+   }
```

ワイ「そんで、その状態を`props`で子コンポーネントに渡してあげるんや」
ワイ「併せて、**状態を更新する関数**も`props`で渡してあげるんや」


```diff_tsx
  return (
    <>
      <label>
        <span>設定内容1</span>
        <ToggleSwitch
+         isOn={formState.state1}
+         onClick={(bool) => switchState("state1", bool)}
        />
      </label>
      <label>
        <span>設定内容2</span>
        <ToggleSwitch
+         isOn={formState.state2}
+         onClick={(bool) => switchState("state2", bool)}
        />
      </label>
      <label>
        <span>設定内容3</span>
        <ToggleSwitch
+         isOn={formState.state3}
+         onClick={(bool) => switchState("state3", bool)}
        />
      </label>
    </>
  )
}

```

### `ToggleSwitch`コンポーネント

ワイ「`ToggleSwitch`コンポーネントは、親から`props`受け取るように変えてやるんや」


```diff_tsx
    // Propsの型を定義
+   type ToggleSwitchProps = {
+     // オン・オフを表す真偽値
+     isOn: boolean
+     // オン・オフをクリックした時に発火する関数
+     onClick: (bool: boolean) => void
+   }

    const ToggleSwitch: React.FC<ToggleSwitchProps> = ({
+     isOn, onClick
    }) => {

      /* 子コンポーネントで持っていた状態は削除 */

      return (
        <button type="button" onClick={() => onClick(!isOn)}>
          {isOn ? "ON" : "OFF"}
        </button>
      )
    }
```
娘「ふ〜ん」
娘「子コンポーネントは状態を持たない」
娘「シンプルに、親から受け取った状態を表示する」
娘「そして、ボタンがクリックされたら、親から受け取った関数を実行する」
娘「そんな感じになったね」

ワイ「せや」
ワイ「こうすることで、ページ側で全ての状態を表示してやることも簡単や」


### こんな感じ
![スクリーンショット 2023-06-29 12.21.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/295452/d06e6061-84bd-5ab7-2f97-84863700656e.png)

↑全ステートをページ側に表示できる

# じゃあ、親コンポーネントがステートを持つべきってこと？

娘「なるほど〜」
娘「じゃあ、末端のコンポーネントに状態を持たせないで」
娘「ページ側とかで状態を管理すべきなんだね！」

ワイ「いや、そうとも限らへんねん」
ワイ「場合によっては、子コンポーネント側に状態を持ったほうがいいこともあるんや」

娘「そうなんだ」

ワイ「例えば、**マウスの座標を可視化してくれるコンポーネント**ついて考えてみるで」

# マウスの座標を可視化するコンポーネント

ワイ「まずは`props`の型を定義するでぇ」
ワイ「子で状態を管理するんやなくて、親から`props`で受け取るイメージや！」

```tsx
// Propsの型を定義
type MouseTrackerProps = {
  // X座標の数値
  x: number,
  // Y座標の数値
  y: number,
  // マウスが動いた時に発火する関数
  onMouseMove: (x: number, y: number) => void
}
```

ワイ「↑こうやな」

ワイ「次に、コンポーネント部分のコードは───」

```tsx
// マウス座標を可視化するコンポーネント
const MouseTracker: React.FC<PropsWithChildren<MouseTrackerProps>> = ({
  children,
  x,
  y,
  onMouseMove,
}) => {
  return (
    <div onMouseMove={event => onMouseMove(event.clientX, event.clientY)}>
      <p>X座標: {x} px</p>
      <p>Y座標: {y} px</p>
      <p>この中のマウスの動きを計測する</p>
      {children}
    </div>
  );
}
```

ワイ「↑こうやな」

娘「ふーん」
娘「要は───」

- `children`を受け取って、マウス座標と一緒に表示してくれるコンポーネント
    - 親から受け取った座標を表示する
    - マウスが動いたら、親から受け取った関数を実行する

娘「↑こういうコンポーネントなんだね」

ワイ「せや」
ワイ「ページ側からは、こんな感じで呼び出すんや」

### ページ側

```tsx
const SamplePage = () => {
  // ページ側でマウスの座標の状態を管理する
  const [position, setPosition] = useState({ x: 0, y: 0 });

  return (
    <>
      <MouseTracker
        // propsとして子に渡してやる
        x={position.x}
        y={position.y}
        onMouseMove={(x, y) => setPosition({ x, y })}
      >
        {/* children */}
        <ChildComponent />
        <ChildComponent />
        <ChildComponent />
        <ChildComponent />
        <ChildComponent />
        <ChildComponent />
      </MouseTracker>
    </>
  )
}
```

### 見た目はこんな感じ

![スクリーンショット 2023-06-29 13.15.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/295452/b031b5b3-c0cf-6df6-a4fe-0e9a5e367b8a.png)


娘「なるほどね」
娘「なんに使うのか分からないけど、マウスの座標を表示できたね」
娘「じゃあ、これでいいんじゃないの？」
娘「ページに状態を持たせる感じで」

ワイ「いや、これだと問題があんねん」

# マウスが動くたびに、ページ全体が再レンダリングされる

ワイ「この`MouseTracker`コンポーネントの上でマウスを動かすと」
ワイ「1秒間に何十回も**ページのステートが更新される**から」
ワイ「ページ全体が、エグいほど再レンダリングされるんや」

娘「なるほど・・・！つまり───」

> React「おっ」
> React「マウスが動いたことで、ページが持ってる状態が更新されたな！」
> React「ほな、ページ全体を再レンダリングや！再レンダリングや！」

娘「↑こういうことだね」

ワイ「せや」
ワイ「`MouseTracker`の**外側のコンポーネントも再レンダリングされる**んやで」
ワイ「ページコンポーネント内に書かれてるやつは全部や」

娘「へぇ〜・・・！」
娘「じゃあ、ページ内のパーツたちを全部`React.memo()`しないといけないってこと・・・？」

ワイ「いや、そんなことはしなくていいんや」
ワイ「子コンポーネント、つまり`MouseTracker`の方に**ステートを移して**やればいいんや」

娘「へぇ〜」

ワイ「ほな、やってみるでぇ」

### ページ側からは状態を削除

ワイ「ページのコンポーネントからは、状態を削除や！」
ワイ「`props`も渡さへん！」

```diff_tsx
const SamplePage = () => {
- const [position, setPosition] = useState({ x: 0, y: 0 });

  return (
    <>
      <MouseTracker
-       x={position.x}
-       y={position.y}
-       onMouseMove={(x, y) => setPosition({ x, y })}
      >
        <ChildComponent />
        <ChildComponent />
        <ChildComponent />
        <ChildComponent />
        <ChildComponent />
        <ChildComponent />
      </MouseTracker>
    </>
  )
}
```

### `MouseTracker`コンポーネントに状態を持たせる

ワイ「そして、`MouseTracker`に状態を持たせるんや」


```diff_tsx
    // マウス座標を可視化するコンポーネント
-   const MouseTracker: React.FC<PropsWithChildren<MouseTrackerProps>> = ({
+   const MouseTracker: React.FC = ({
      children,
-     x,
-     y,
-     onMouseMove,
    }) => {
+     const [position, setPosition] = useState({ x: 0, y: 0 });
+     const onMouseMove = (x: number, y: number) => setPosition({ x, y })
+     const { x, y } = position;

      return (
        <div onMouseMove={event => onMouseMove(event.clientX, event.clientY)}>
          <p>この中のマウスの動きを計測する</p>
          <p>X座標: {x} px</p>
          <p>Y座標: {y} px</p>
          {children}
        </div>
      );
    }
```

ワイ「↑こうすることで、**ページ側の状態変化は起こらへん**ことになるから」
ワイ「無駄な再レンダリングが起こらなくなるんや」

娘「そっか」

ワイ「`children`も再レンダリングされなくなるんやで」

娘「へ〜」
娘「`MouseTracker`コンポーネントだけ、つまり**枠だけを再レンダリング**してくれるんだね」

ワイ「せやで」

娘「じゃあ、頻繁な状態変更が起こりそうな場合は」
娘「上の方に状態を持たないほうがいいんだね」

ワイ「せやな〜」
ワイ「無駄に再レンダリング範囲が広がってしまうからな」

# まとめ

- 子にステートを持たせると、親でまとめて表示とかしづらい
    - 親にステートを持たせて、`props`で子に渡そう
- 親が「めっちゃ更新されるステート」を持つと、親がめっちゃ再レンダリングされて無駄
    - そういう場合は子にステートを持たせよう
        - `children`も活用しよう
        
**〜おしまい〜**


# 参考文献

- [【React】「困難は分割せよ」―― 複雑な画面は小さな機能に分けて実装しよう。](https://qiita.com/honey32/items/2e6206c7dc1974b9bf9a)
- [Before You memo()](https://overreacted.io/before-you-memo/)