※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
も活用しよう
-
- そういう場合は子にステートを持たせよう
〜おしまい〜