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 で記述し直す、という方法を使ってみてください。