オプショナルなReactNode型のpropを正しく扱う
背景
例1:Button に iconを渡したときだけスペースをちょっと開けてアイコンを表示し、渡さなかったら非表示にする
<Button icon={<PlusIcon />}>Add</Button>
→ <button><span style={{marginRight: "0.5rem"}}><PlusIcon /></span>Add</button>
https://gyazo.com/402cda6fc22be72962bc485a7181dcb7
<Button>Add</Button>
→ <button>Add</button>
https://gyazo.com/a2997c282ba126031d6ab0092023ff69
例2:Checkbox に children を渡したときだけ全体を label でラップし、渡さなかったらラップしない
<Checkbox name="agreement">同意する</Checkbox>
→ <label><input type="checkbox" name="agreement"/>同意する</label>
https://gyazo.com/e943c5cc726768489a97067c42654b38
<Checkbox name="agreement" htmlFor={id}/>
→ <input type="checkbox" name="agreement" htmlFor={id}/>
https://gyazo.com/20281a602e68006eb88e2aff92f48dc1
例3:Layout にページごとの header、footer を設定できるようにする(コンポジションパターン)
課題
この場合、単純に && といった条件演算子や、 boolean への型変換(Boolean(value)や!!value)を使って「表示されない ReactNode 」を判定すると不都合な場合がある
これは、値がtruthy/falsy であることと表示される/されないことが違うことが原因
例:ReactNode は string、number も入るから…
prop に 0 や ""(空文字列)を入れると
falsy なのに、レンダリングされる
結果として、謎の 「0」 が表示されるなど
例:ReactNodeは boolean が入るから…
prop に true を入れると
truthy なのに、レンダリングされない
結果として、背景 の例1では謎の空 span と空白が生まれ、例2では謎の空 label が生まれる
つらい
解決方法
ReactNode のうち、表示されないものは以下の4つ
true
false
null
undefined
true, false, null, or undefined (which are not displayed)
これらをちゃんと判別するようにすればよい
code:Button.tsx
<button>
{icon !== undefined && icon !== null && typeof icon !== "boolean" && (
// ↑ これ
<span style={{ merginRight: "0.5rem" }}>{icon}</span>
)}
{children}
</button>
code:Checkbox.tsx
export const Checkbox = forwardRef<HTMLInputElement, Props>(
({ children, ...props }, ref) => {
if (children !== undefined && children !== null && typeof children !== "boolean") {
// ↑ これ
return <input type="checkbox" {...props} ref={ref} />;
}
return (
<label>
<input type="checkbox" {...props} ref={ref} />
{children}
</label>
);
},
);
毎回書くのは大変なので、ユーティリティ関数にする
code:isRenderableReactNode.ts
import { type ReactNode } from "react";
type RenderableReactNode = Exclude<ReactNode, null | undefined | boolean>;
/**
* 表示されない React node (true, false, null, undefined) の時に false を、それ以外は true を返す
*
*/
export const isRenderableReactNode = (
node: ReactNode,
): node is RenderableReactNode => node !== undefined && node !== null && typeof node !== "boolean";
関数名は isRenderableReactNode のほかに isReactNodeToRender とかでもいいかも
ユーティリティ関数を使うと、次のように書ける
code:Button.tsx
<button>
{!isRenderableReactNode(icon) && (
// ↑ これ
<span style={{ merginRight: "0.5rem" }}>{icon}</span>
)}
{children}
</button>
code:Checkbox.tsx
export const Checkbox = forwardRef<HTMLInputElement, Props>(
({ children, ...props }, ref) => {
if (!isRenderableReactNode(children)) {
// ↑ これ
return <input type="checkbox" {...props} ref={ref} />;
}
return (
<label>
<input type="checkbox" {...props} ref={ref} />
{children}
</label>
);
},
);
Happy
FYI