※7割くらい ChatGPT で書いた記事です。
ある日の我が家
娘「ねえ、パパ」
ワイ「なんや、娘ちゃん?」
娘「コンポーネントには、状態っていう概念があるじゃない?」
ワイ「あるなぁ」
娘「親コンポーネントが状態を持つべきなの?」
娘「それとも子が持つべきなの?」
娘「どう決めるべきか分からないんだよね」
ワイ「おお、ええ質問やな」
ワイ「具体例を見ながら話を進めてみよか」
娘「うん!」
娘「娘、具体例、好き!」
ワイ「まずは、トグルスイッチのコンポーネントを例に考えてみるで」
子コンポーネントに状態を持たせる場合
ワイ「コードはこんな感じや」
// トグルスイッチのコンポーネント
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を呼び出してやるんや」
ページ側
const SamplePage = () => {
  return (
    <form>
      <label>
        <span>設定内容1</span>
        <ToggleSwitch />
      </label>
      <label>
        <span>設定内容2</span>
        <ToggleSwitch />
      </label>
      <label>
        <span>設定内容3</span>
        <ToggleSwitch />
      </label>
    </form>
  )
}
画面はこんな感じ
娘「なるほどね」
娘「これは・・・よくない気がする!」
娘「子コンポーネントに状態を持たせるより」
娘「ページ側に持たせたほうが良さそうな気がしてきた!」
ワイ「お、そうやな」
ワイ「複数のToggleSwitchがあって、全ての状態を知りたいときに不便やもんな」
娘「そうそう、全てのトグルスイッチの状態を、まとめてフォームで送信したいときとか」
娘「ページ側で状態を管理してないと困っちゃいそう」
ワイ「せやな」
ワイ「ほな、ちょいとコードを修正して」
ワイ「ページ側に状態を持たせてみるでぇ」
ページ側に状態を持たせる場合
ワイ「ページ側で、useState()を使って状態管理してやるんや」
  const SamplePage = () => {
    // ページ側で状態を保つ
+   const [formState, setFormState] = useState({
+     state1: false,
+     state2: false,
+     state3: false,
+   })
    // 状態を更新するためのヘルパー関数
+   const switchState = (key: keyof typeof formState, bool: boolean) => {
+     /* 省略 */
+   }
ワイ「そんで、その状態をpropsで子コンポーネントに渡してあげるんや」
ワイ「併せて、状態を更新する関数もpropsで渡してあげるんや」
  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受け取るように変えてやるんや」
    // 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>
      )
    }
娘「ふ〜ん」
娘「子コンポーネントは状態を持たない」
娘「シンプルに、親から受け取った状態を表示する」
娘「そして、ボタンがクリックされたら、親から受け取った関数を実行する」
娘「そんな感じになったね」
ワイ「せや」
ワイ「こうすることで、ページ側で全ての状態を表示してやることも簡単や」
こんな感じ
↑全ステートをページ側に表示できる
じゃあ、親コンポーネントがステートを持つべきってこと?
娘「なるほど〜」
娘「じゃあ、末端のコンポーネントに状態を持たせないで」
娘「ページ側とかで状態を管理すべきなんだね!」
ワイ「いや、そうとも限らへんねん」
ワイ「場合によっては、子コンポーネント側に状態を持ったほうがいいこともあるんや」
娘「そうなんだ」
ワイ「例えば、マウスの座標を可視化してくれるコンポーネントついて考えてみるで」
マウスの座標を可視化するコンポーネント
ワイ「まずはpropsの型を定義するでぇ」
ワイ「子で状態を管理するんやなくて、親からpropsで受け取るイメージや!」
// Propsの型を定義
type MouseTrackerProps = {
  // X座標の数値
  x: number,
  // Y座標の数値
  y: number,
  // マウスが動いた時に発火する関数
  onMouseMove: (x: number, y: number) => void
}
ワイ「↑こうやな」
ワイ「次に、コンポーネント部分のコードは───」
// マウス座標を可視化するコンポーネント
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を受け取って、マウス座標と一緒に表示してくれるコンポーネント- 親から受け取った座標を表示する
- マウスが動いたら、親から受け取った関数を実行する
 
娘「↑こういうコンポーネントなんだね」
ワイ「せや」
ワイ「ページ側からは、こんな感じで呼び出すんや」
ページ側
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>
    </>
  )
}
見た目はこんな感じ
娘「なるほどね」
娘「なんに使うのか分からないけど、マウスの座標を表示できたね」
娘「じゃあ、これでいいんじゃないの?」
娘「ページに状態を持たせる感じで」
ワイ「いや、これだと問題があんねん」
マウスが動くたびに、ページ全体が再レンダリングされる
ワイ「このMouseTrackerコンポーネントの上でマウスを動かすと」
ワイ「1秒間に何十回もページのステートが更新されるから」
ワイ「ページ全体が、エグいほど再レンダリングされるんや」
娘「なるほど・・・!つまり───」
React「おっ」
React「マウスが動いたことで、ページが持ってる状態が更新されたな!」
React「ほな、ページ全体を再レンダリングや!再レンダリングや!」
娘「↑こういうことだね」
ワイ「せや」
ワイ「MouseTrackerの外側のコンポーネントも再レンダリングされるんやで」
ワイ「ページコンポーネント内に書かれてるやつは全部や」
娘「へぇ〜・・・!」
娘「じゃあ、ページ内のパーツたちを全部React.memo()しないといけないってこと・・・?」
ワイ「いや、そんなことはしなくていいんや」
ワイ「子コンポーネント、つまりMouseTrackerの方にステートを移してやればいいんや」
娘「へぇ〜」
ワイ「ほな、やってみるでぇ」
ページ側からは状態を削除
ワイ「ページのコンポーネントからは、状態を削除や!」
ワイ「propsも渡さへん!」
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に状態を持たせるんや」
    // マウス座標を可視化するコンポーネント
-   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も活用しよう
 
- 
 
- そういう場合は子にステートを持たせよう
〜おしまい〜




