🦁

Reactにおける条件付きレンダリングのパターン

2024/03/31に公開

この記事ではReactで条件付きレンダリングについて、どのような方法があるか様々なパターンで説明したいと思います。 条件付きレンダリングは動的なUIをレンダリングするReactの必須テクニックですが、いつも「なんとなく」同じ方法で条件分岐を実装してしまうなーって方に他の方法での実装も知ってもらえたら幸です。

今回は以下のアプローチを紹介していきます。

  • if/else文
  • switch文
  • 論理積(&&)
  • 条件 (三項) 演算子
  • Null合体演算子 (??)
  • エラーバウンダリ
  • 高階コンポーネント (High Order Components - HOC)

if/else文

従来のif/else文は最も基本的な実装方法です。JSXに慣れていない場合、真っ先に思いつく方法かと思います。

const Component = ({ isLoggedIn }: { isLoggedIn: boolean }) => {
  // ログインの有無で表示を変える
  if (isLoggedIn) {
    return <div>Welcome back!</div>;
  } else {
    return <div>Please sign in.</div>;
  }
}

if/else文で異なるReact要素を返却します。単純な条件でコンポーネントを切り替えたい場合に有効です。
ただしこのアプローチは条件が複雑だったりif/elseがネストする場合に可読性が低下しますし、Reactの宣言的なアプローチとは乖離するので乱用は避けた方がよいかと思います。

switch文

こちらはswitch文を使った条件分岐です。

interface StatusProps {
  status: 'loading' | 'success' | 'error';
}

const StatusMessage = ({ status }:StatusProps) => {
  switch (status) {
    case "loading":
      return <div>Loading...</div>;
    case "success":
      return <div>Data loaded successfully!</div>;
    case "error":
      return <div>Error loading data.</div>;
    default:
      // すべてのケースを網羅していることを確認
      throw new Error(status satisfies never);
  }
};

複数の条件分岐がある場合にif/else分で実装するより可読性が良くなります。また、TypeScriptのsatisfiesを用いてケース漏れを防ぐことができるためおすすめです。
ただし、if/else同様に宣言的なアプローチとは乖離するのでケースには気をつけたいところです。


ここからはJSX内で利用するテクニックを紹介します。
上記の方法よりも宣言的に実装でき、JSXを部分的に動的なものにできるため基本的にはこれらを活用していくのをおすすめします。

論理積(&&)

JSX内で論理積(&&) を使って条件分岐を行います。
条件が一致する場合のみにレンダリングしたい要素がある場合に有効です。

interface UserPanelProps {
  isLoggedIn: boolean;
  userName?: string;
}

const UserPanel = ({ isLoggedIn, userName }:UserPanelProps) => {
  return (
    <div>
      {/* ログインしている場合のみユーザー名とログアウトボタンを表示する */}
      {isLoggedIn && (
        <div>
          <h2>Welcome, {userName}!</h2>
          <button>Logout</button>
        </div>
      )}
    </div>
  );
};

この方法では条件式の結果(上記ではisLoggedInの値)がboolean以外の場合に注意が必要です。
例えばcountがnumber型で、{count && <p>count is {count}</p>} としている場合、count0のとき条件が偽となり、"count is 0"という文字列は表示されずに"0"という文字だけが表示されます。
このようにfalsyな場合の値について意識する必要があります。

条件 (三項) 演算子

JSX内で条件 (三項) 演算子を使って条件分岐を行います。

interface UserPanelProps {
  isLoggedIn: boolean;
  userName?: string;
}

const UserPanel = ({ isLoggedIn, userName }: UserPanelProps) => {
  return (
    <div>
      {isLoggedIn ? (
        // ログインしている場合のみユーザー名とログアウトボタンを表示する
        <div>
          <h2>Welcome, {userName}!</h2>
          <button>Logout</button>
        </div>
      ) : (
        // ログインしていない場合はログインボタンを表示する
        <button>Login</button>
      )}
    </div>
  );
};

先ほどの論理積(&&) のケースではレンダリングしたい場合で利用できましたが、こちらはif/else分のように条件の真偽でレンダリング内容を分けたい場合に有効です。
ただし、JSX内で条件演算子がネストすると一気に可読性が下がるため条件の見直しや小さなサブコンポーネントに分割するなおのアプローチをおすすめします。

Null合体演算子 (??)

JSX内でNull合体演算子を使ってデフォルト値を設定します。

interface UserProfileProps {
  userName?: string | null;
}

const UserProfile = ({ userName }: UserProfileProps) => {
  return (
    <div>
      {/* ユーザー名がnullまたはundefinedの場合、デフォルトの名前を表示する */}
      <h2>Welcome, {userName ?? 'Guest'}!</h2>
    </div>
  );
};

こちらの方法はオプショナルな値やAPIからの応答などnullやundefinedが頻出する際に、フォールバックなものとしてコンテンツをレンダリングしたい場合に有効です。


ここからは、部分的な条件分岐ではなくコンポーネント全体に対する条件分岐を行う高度なパターンを紹介します。これらはプロジェクトが大規模な場合に有効となります。

エラーバウンダリ

reactのErrorBoundaryコンポーネントを使ったパターンです。

<ErrorBoundary fallback={<p>エラーが発生しました。</p>}>
  <MyComponent />
</ErrorBoundary>

この方法は、MyComponentのレンダリング中にエラーがスローされた場合に、コンポーネント全体を別のフォールバックコンポーネントに差し替えたい場合に利用します。
ErrorBoundaryコンポーネント自体の解説は本記事の内容から逸れるため省略しますが、参考程度に実装例を記載しておきます。

ErrorBoundary`コンポーネントの実装
import { Component, ErrorInfo, ReactNode } from "react";

interface ErrorBoundaryProps {
  children: ReactNode;
  fallback: ReactNode; // エラー時に表示するフォールバックUI
}

interface ErrorBoundaryState {
  hasError: boolean;
}

class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
  constructor(props: ErrorBoundaryProps) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(_: Error): ErrorBoundaryState {
    // 次のレンダリングでフォールバックUIを表示できるように状態を更新する
    return { hasError: true };
  }

  componentDidCatch(error: Error, errorInfo: ErrorInfo) {
    // エラーをロギングサービスに記録するなどのエラーハンドリングをここで行う
    console.error("Uncaught error:", error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      // フォールバックUIのレンダリング
      return this.props.fallback;
    }

    return this.props.children;
  }
}

実装の詳細は公式ドキュメントを参照頂ければと思います。
https://ja.react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary

また、ErrorBoundaryを簡単に扱えるライブラリもあります。(https://www.npmjs.com/package/react-error-boundary)
https://www.npmjs.com/package/react-error-boundary

高階コンポーネント (High Order Components - HOC)

デザインパターンであるHOCパターンを活用した方法です。HOCパターンは高階関数(higher-order function)のコンポーネント版で、コンポーネントを引数として受け取り、新しいコンポーネントを返すデザインパターンです。

import React, { ComponentType } from 'react';

// HOCの型定義
interface WithAuthenticationProps {
  isLoggedIn: boolean;
}

// HOCを定義する。この関数はコンポーネントと、そのコンポーネントに適用する追加のpropsを引数に取る
function withAuthentication<T extends JSX.IntrinsicAttributes>(
  WrappedComponent: ComponentType<T>
) {
  // 返されるコンポーネント
  return ({ isLoggedIn, ...props }: WithAuthenticationProps & T) => {
    if (isLoggedIn) {
      // ユーザーがログインしている場合、WrappedComponentをレンダリング
      return <WrappedComponent {...props as unknown as T} />;
    } else {
      // ユーザーがログインしていない場合、ログイン促進メッセージを表示
      return <div>Please log in to view this content.</div>;
    }
  };
}

// 使用例: このコンポーネントは認証されたユーザーのみがアクセス可能
interface ProtectedComponentProps {
  // このコンポーネント特有のpropsを定義する場合はここに追加
}

const ProtectedComponent: React.FC<ProtectedComponentProps> = () => {
  return <div>This is a protected content only visible to authenticated users.</div>;
};

// HOCを使用してProtectedComponentを拡張
export const ProtectedWithAuth = withAuthentication(ProtectedComponent);

// 使用時には、`isLoggedIn` propsを通じてユーザーのログイン状態を渡す
// <ProtectedWithAuth isLoggedIn={true} />
// <ProtectedWithAuth isLoggedIn={false} />

上記の例では、ProtectedComponentコンポーネントを拡張し、条件に応じてProtectedComponentとしてレンダリングする内容を動的に変更しています。
これだけではHOCのメリットが分かりづらいかもしれませんが、HOCが効果を発揮するのはロジック(今回の例では条件分岐)をカプセル化し、任意のコンポーネントに対してロジックを付与したいケースです。これはロジックが上記の例のように単純ではなく、かつ多くのコンポーネントにそのロジックを持たせたい場合にHOCは効果を発揮します。
言い換えれば、付与したい条件分岐が単純であったり拡張したいコンポーネントが複数あるわけではない場合は、先述の他の方法でコンポーネント自体に条件分岐を持たせる方法が適切です。
HOCの詳細な説明は以下が参考になりますので詳しく知りたい場合は参考にしてください。
https://www.patterns.dev/react/hoc-pattern/

終わりに

いかがだったでしょうか?Reactでは複数の条件分岐のパターンがあるため、それぞれが有用な場合に使い分けができると良い実装・設計に繋がると思います。

Discussion