booleanを極力使わないReactの書き方
目次
こんにちは、@Himenonです。表題の通り、React の実装中で boolean を極力使用しない方法を紹介します。
※ 注意「boolean を使うな!」という話ではありません。こういう方法もあるよ、という話です。
boolean で表現される状態は String Literal を利用する
UI の状態を表すために、複数の boolean が条件に利用される場合、各状態に名前をつけて状態を表現するほうが、可読性が高く、必要最小限の実装で済みます。これについて、最初にサンプルを見ていくとわかりやすいので、いくつか紹介していきます。
String Literal で書き直される例
サンプル 1:開閉状態
例えば次のように開閉状態が State 表現されている場合、
Sample Code: https://codesandbox.io/s/without-boolean-pattern-jkg57q?file=/src/before/Sample1.tsx
export type MyComponentProps = {};
const MyComponent: React.FC<MyComponentProps> = (props) => {
const [open, setOpen] = React.useState(false);
return (
<div>
<button onClick={() => setOpen((prev) => !prev)}>
{open ? "CLOSE" : "OPEN"}
</button>
{open && <div>HELLO</div>}
</div>
);
};
これを String Literal で記述すると
Sample Code: https://codesandbox.io/s/without-boolean-pattern-jkg57q?file=/src/after/Sample1.tsx
export type MyComponentProps = {};
const MyComponent: React.FC<MyComponentProps> = (props) => {
const [open, setOpen] = React.useState<"OPEN" | "CLOSE">("CLOSE");
return (
<div>
<button
onClick={() => setOpen((prev) => (prev === "OPEN" ? "CLOSE" : "OPEN"))}
>
{open ? "CLOSE" : "OPEN"}
</button>
{open === "OPEN" && <div>HELLO</div>}
</div>
);
};
となります。
サンプル 2:開閉状態
開閉状態が Props で表現されている場合
Sample Code: https://codesandbox.io/s/without-boolean-pattern-jkg57q?file=/src/before/Sample2.tsx
export type MyComponentProps = {
openDialog: boolean;
};
const MyComponent: React.FC<MyComponentProps> = (props) => {
return <div>{props.openDialog && <Dialog heading="hello" />}</div>;
};
これを String Literal で記述し直すと、
Sample Code: https://codesandbox.io/s/without-boolean-pattern-jkg57q?file=/src/after/Sample2.tsx
export type MyComponentProps = {
dialogStatus: "OPEN" | "CLOSE";
};
const MyComponent: React.FC<MyComponentProps> = (props) => {
return (
<div>{props.dialogStatus === "OPEN" && <Dialog heading="hello" />}</div>
);
};
サンプル 3:複数の開閉状態
Sample Code: https://codesandbox.io/s/without-boolean-pattern-jkg57q?file=/src/before/Sample3.tsx
export type MyComponentProps = {
openAlertDialog: boolean;
openWarningDialog: boolean;
alertDialogProps?: DialogProps;
warningDialogProps?: DialogProps;
};
const MyComponent: React.FC<MyComponentProps> = (props) => {
return (
<div>
{props.openAlertDialog && props.alertDialogProps ? (
<Dialog {...props.alertDialogProps} />
) : props.openWarningDialog && props.warningDialogProps ? (
<Dialog {...props.warningDialogProps} />
) : null}
</div>
);
};
これを String Literal で記述し直すと、次のようになります。
Sample Code: https://codesandbox.io/s/without-boolean-pattern-jkg57q?file=/src/after/Sample3.tsx
type AlertDialogPropsWithKind = {
kind: "OPEN_ALERT";
props: DialogProps;
};
type WarningDialogPropsWithKind = {
kind: "OPEN_WARNING";
props: DialogProps;
};
type CloseDialogPropsWithKind = {
kind: "CLOSE";
};
export type MyComponentProps = {
dialog:
| AlertDialogPropsWithKind
| WarningDialogPropsWithKind
| CloseDialogPropsWithKind;
};
const MyComponent: React.FC<MyComponentProps> = (props) => {
const { dialog } = props;
return (
<div>
{dialog.kind === "OPEN_ALERT" && <Dialog {...dialog.props} />}
{dialog.kind === "OPEN_WARNING" && <Dialog {...dialog.props} />}
</div>
);
};
※ 2023/05/24 Dialog の型定義を Union Type に変更
サンプル 4:ステータスで表現する
Sample Code: https://codesandbox.io/s/without-boolean-pattern-jkg57q?file=/src/before/Sample4.tsx
type MyComponentProps = {
isWarn: boolean;
isError: boolean;
};
const MyComponent: React.FC<MyComponentProps> = (props) => {
return (
<div>
{props.isWarn && <WarnComponet />}
{props.isError && <ErrorComponent />}
</div>
);
};
これを String Literal で記述し直すと次のようになります。
Sample Code: https://codesandbox.io/s/without-boolean-pattern-jkg57q?file=/src/after/Sample4.tsx
type Status = "WARN" | "ERROR";
export type MyComponentProps = {
status: Status;
};
const MyComponent: React.FC<MyComponentProps> = (props) => {
const statusMap: Record<Status, React.ReactNode> = {
WARN: <WarnComponet />,
ERROR: <ErrorComponent />,
};
return <div>{statusMap[props.status]}</div>;
};
String Literal で記述した場合の Pros & Cons
サンプル 1, 2
- boolean を”OPEN” | “CLOSE”で表現し直した
- props.open で表現できた部分が、 props.open === “OPEN”と書き直す必要がある
サンプル 3
- Component を表示するための複雑な条件分岐が解体された
- 表示する Dialog の Props を Discriminated Union で表現し拡張性を手に入れた
サンプル 4
- 複数の boolean が 1 つの status にまとめられた。
- Status という String LIteral に分離されたことより、型定義が適用された
String Literal で記述すると何が良いか?
単純な開閉な場合はあまり恩恵を感じることはありませんが、条件分岐が複数絡み合うようなケースに威力を発揮してきます。
例えば、props に 2 つの boolean を受け取る口が有り、この 2 つがコンポーネント内部で次のように利用されているとします。
props.condition1 && props.condition2
このコンポーネント内部から見たときは十分に機能するように見えますが、コンポーネントを外側からみると、condition1 と condition2 の 2 つの boolean があるためコンポーネントは 2x2 の 4 つの組み合わせが存在するように見えます。 Interface だけみると複雑度が高い状態になっています。
これを回避するためには、props.condition1 と props.condition2 の組み合わせで表現したかった状態を表現し直すことです。手っ取り早いのが String で状態を定義することで、必要な状態を必要な分だけ表現することで、複雑度を下げることが可能です。
また、TypeScript の型定義を組み合わせることにより、追加した状態に応じて型定義も変化するため、実装の影響範囲が型レベルで判別することが可能です。
React の Component の有無は props の有無で判別する
React の Component の有無は boolean を利用せずとも表現することができます。例えば次のようなコンポーネントがあるとします。
Sample Code: https://codesandbox.io/s/without-boolean-pattern-jkg57q?file=/src/before/Sample5.tsx
export type MyComponentProps = {
openDialog: boolean;
dialog: DialogProps;
};
const MyComponent: React.FC<MyComponentProps> = (props) => {
return <div>{props.openDialog && <Dialog {...props.dialog} />}</div>;
};
props の field の有無で判別すると次のように記述することができます。
Sample Code: https://codesandbox.io/s/without-boolean-pattern-jkg57q?file=/src/after/Sample5.tsx
export type MyComponentProps = {
dialog?: DialogProps;
};
const MyComponent: React.FC<MyComponentProps> = (props) => {
return <div>{props.dialog && <Dialog {...props.dialog} />}</div>;
};
これは React の Component のレンダリング時に、Falsy Like(false, null、 undeifned)な結果になるコンポーネントはレンダリングされないことを利用しています。
ただし、コンポーネントの表示・非表示時にアニメーションをしている場合は、この方法を利用するとアニメーションが無視されるため中間状態が必要な場合は内部のコンポーネントが開閉の props を取ったほうが良いでしょう。
まとめ
boolean を利用しないで React のコンポーネントを実装する方法を紹介しました。純粋な開閉状態を除き、条件が複数重なるようなケースでは使われないパターンが発生したり、複雑な条件分岐を内包してしまう可能性があるため、これを String Literal で表現し直し必要十分な状態に書き換える方法を紹介しました。
条件分岐が複雑になりそうと感じたら String Literal で記述し直す、という方法を使ってみてください。