63
30

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

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

Last updated at Posted at 2023-06-29

※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>> = ({
+   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も活用しよう

〜おしまい〜

参考文献

63
30
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
63
30

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?