21
お題は不問!Qiita Engineer Festa 2023で記事投稿!

更新日

投稿日

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

※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>
  )
}

画面はこんな感じ

スクリーンショット 2023-06-29 11.52.27.png

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

ワイ「お、そうやな」
ワイ「複数の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>
      )
    }

娘「ふ〜ん」
娘「子コンポーネントは状態を持たない」
娘「シンプルに、親から受け取った状態を表示する」
娘「そして、ボタンがクリックされたら、親から受け取った関数を実行する」
娘「そんな感じになったね」

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

こんな感じ

スクリーンショット 2023-06-29 12.21.36.png

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

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

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

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

娘「そうなんだ」

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

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

ワイ「まずは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>
    </>
  )
}

見た目はこんな感じ

スクリーンショット 2023-06-29 13.15.25.png

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

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

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

ワイ「この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>> = ({
      children,
-     x,
-     y,
-     onMouseMove,
    }) => {
+     const [position, setPosition] = useState({ x: 0, y: 0 });
+     const onMouseMove = (x: number, y: number) => setPosition({ x, y })

      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も活用しよう

〜おしまい〜

参考文献

新規登録して、もっと便利にQiitaを使ってみよう

  1. あなたにマッチした記事をお届けします
  2. 便利な情報をあとで効率的に読み返せます
ログインすると使える機能について

コメント

この記事にコメントはありません。
あなたもコメントしてみませんか :)
新規登録
すでにアカウントを持っている方はログイン
21