Props のバケツリレー (Props Drilling) を解決するときに、安易に Context を使ったり、状態管理ライブラリ(Recoil, Jotai, Redux)に頼っていませんか?
そんなことをせずとも、「CompA が CompB を使い、CompB が CompC を使い、 CompC が ...」という依存関係のチェーンを浅くするのが最善の解決策である場合があります。
KISS (Keep It Simple Stupid) という名言があるように、「Props を渡すだけ」というわかりやすい方法を取ることで、将来のコード読解が楽になり、メンテナンスが容易になり、そもそもバグの混入を防げるので、一石三鳥です。
前回の記事で述べた「そもそも無駄なコンポーネントを挟まない」テクニックでも残ってしまった Props のバケツリレーを削減する方法として、この記事では コンポジション (composition)、すなわち ReactNode 型の Props を使う方法を解説します。
1. そもそも無駄なコンポーネントを挟まない
2. コンポジション(ReactNode 型 Props) を使う ← 今ここ
3. ステートを適切なコンポーネントに置く
このうち、 3 については、すでに記事化している内容と重複するので、そちらを参照してください (バケツリレーには言及していませんが、不要な Props の受け渡しが削られていることが明らかだと思います。)
Not Good 「OK」と「キャンセル」を一つのコンポーネントに入れると...
この画像のように、画面の一番下にボタンを2つ配置するコンポーネント FloatingButtons
を、以下のようなコードで作ってみました。
type Props = {
onCancel: () => void,
onOk: () => void,
};
const FloatingButtons: FC<Props> = ({
onCancel,
onOk,
}) => {
return (
<div /*スタイル指定は省略*/ >
<button type="button" onClick={onCancel}>
キャンセル
</button>
<button type="button" onClick={onOk}>
OK
</button>
</div>
);
};
ワイ「アカン、この FloatingButtons はフォームの送信のときにも使うんやった...」
ワイ「OK ボタンにわたす Props をいじらなアカンな...」
type Props = {
onCancel: () => void,
+ isOkSubmit?: boolean,
- onOk: () => void,
+ onOk?: () => void,
};
const FloatingButtons: FC<Props> = ({
onCancel,
+ isOkSubmit = false,
onOk,
}) => {
return (
<div /* スタイル指定は省略 */ >
<button type="button" onClick={onCancel}>
キャンセル
</button>
- <button type="button" onClick={onOk}>
+ <button
+ type={isOkSubmit ? "submit" : "button"}
+ onClick={onOk}
+ >
OK
</button>
</div>
);
};
これはコードの複雑化、保守性の低下の序章に過ぎません。
「OK ボタンの文言を変えたい」「やっぱりキャンセルボタンは URL で画面遷移したいので Link タグで」「OK ボタンは、押したあと処理が終わるまでクリック不能にしたい」...
現実の仕様を満たせるように作り込んでいくにつれて、ボタンをカスタマイズするための Props が雪だるま式にどんどん増えていき、 FloatingButtons コンポーネントの記述が複雑化しています。
// 肥大化した Props の成れの果て ...
type Props = {
cancelText: string,
cancelhref?: string,
onCancel: () => void,
okText: string,
okHref?: string,
isOkDisabled?: boolean
isOkSubmit?: boolean,
onOk?: () => void,
};
どうすれば良かったのでしょうか?
OK コンポジションパターン
キャンセルの <button>
と OK の <button>
を、それぞれ ReactNode 型の Props に置き換えて、親からそっくり丸ごと注入できるようにしてみましょう。
ReactNode とは、ざっくり言うと「React に描画させてよい任意の要素」 を表す型であり、以下のような型のユニオンです。
- 描画される要素
- 例:
<div>ディブ要素</div>
,<>フラグメント</>
ほげ
,23
- 例:
- エラーを出さず何も描画されない要素
- 例:
true
,false
,null
,undefined
- 例:
参考: https://dackdive.hateblo.jp/entry/2019/08/07/090000 -
TypeScript: ReactNode型とReactElement型とReactChild型
type Props = {
negativeButton: ReactNode,
positiveButton: ReactNode,
};
const FloatingButtons: FC<Props> = ({
negativeButton,
positiveButton,
}) => {
return (
<div /* スタイル指定は省略 */ >
{negativeButton}
{positibeButton}
</div>
);
};
すると、このコンポーネントを使う側のコードは、以下のようになります。
// 使う側はこんな感じ
<FloatingButtons
negativeButton={(
<button type="button" onClick={handleCancel}>
キャンセル
</button>
)}
positiveButton={(
<button type="button" onClick={handleOK}>
OK
</button>
)}
// 送信ボタンにしたいとき
// positiveButton={(
// <button type="submit">OK</button>
// )}
/>
FloatingButtons の責務は「各ボタンをよしなに配置する」ことだけになり、それぞれのボタンの詳細 (a か button か, type="submit", aria タグ, etc. )については知らなくても良くなります。
それぞれのボタンをカスタマイズしたければ、コンポーネントを利用する側で <button ...
の JSX 式を編集すればいいだけです。簡単でしょ?
このように、中身の要素をあれこれカスタマイズするために複雑怪奇な Props が必要になってしまった場合には、表示する内容をそのまま ReactNode 型の Props として親から注入するようにすることで Props の複雑な取り扱いを減らすことができます。
OK children で直感的に書けるようになる
コンポジションの話をするときに忘れてはいけないのが children Prop です。
<HogeComponent>なかみ</HogeComponent>
のような JSX 式を書いたとき、「なかみ」の文字列 (string) は HogeComponent の children Props として渡されます。
なので、下のように children Prop を使うことで、「FloatingButtons の中身」と直感的に理解できるような書き方ができるコンポーネントを作ることができます。
type Props = {
children: ReactNode,
};
const FloatingButtons: FC<Props> = ({
children
}) => {
return (
<div /* スタイル指定は省略 */ >
{children}
</div>
);
};
// 使う側はこんな感じ
<FloatingButtons>
<button type="button" onClick={handleCancel}>
キャンセル
</button>
<button type="button" onClick={handleOK}>
OK
</button>
</FloatingButtons>
まとめ
前回で説明した「そもそも無駄なコンポーネントを挟まない」では、単純に依存関係のチェーンを短くしましたが、今回の「コンポジション (ReactNode 型 Props) を使う」では、深くなりすぎた依存関係のチェーンを多岐に分かれた浅いチェーンにしてチェーンを浅くしました。
特にコンポジションを使うと、「見た目上は A の中に B が入っているけど、親(P) から A を経由せずに B を直に制御できる」ように書き換えることができます。
つまり、この方法では、 Props のバケツリレーを減らすことができるだけでなく、
- コンポーネントの責務をスリム化し、適切に分担できる
- たとえば「スタイル上の囲い」と「中身の詳細」
- 例: FloatingButton の責務は前者だけになりました
- 実は、余計な「親→子」の再レンダリングも減らすことが可能
- P が再レンダリングすると A, B が強制的に再レンダリングする
- A が再レンダリングしても、B の再レンダリングが起こらない
- B 内で context 等を使うと再レンダリングすることも可能
- 実は、「Client Component の下に Server Component を置けない」規則を満たせる
- ...!?
予告: Server Component とネスト
- 「Server Component では useState のような動的なコードを書けない」
- 「Client Component から Server Component を描画することが出来ない」
だから、「Next.js App Router で Server Component を使うと、動的なサイトを作るのが難しくなる」という懸念が広がっているように見受けられます。
しかし、 composition を使うと、 Client Component の中に Server Component を props として差し込むことが出来ることができます。
これによって、「サーバーと直接やりとりできる」 Server Component の利点と、「状態が変化することができる」 Client Component の利点を組み合わせて自由自在に画面を記述することができるのです。
乞うご期待。 投稿しました。読んでね。
関連スライド、メモ