⚛️

第3回 Container/Presentational Pattern | React デザインパターン入門

2023/12/25に公開

この記事は、React デザインパターン入門シリーズの第3回です!

第2回では、HOC Pattern を紹介しました. 第3回目は、Container/Presentational Pattern です.
(第1,2回の内容は、この記事には一切関係ないのでご安心ください)疎結合

hooks 時代に入っても、Container/Presentational Pattern は武器になる!

Container/Presentational Pattern の概要

このパターンは、ビューとロジックの関心の分離をすることで以下のようなメリットを享受しようとしています.

  • UI の使い回しを容易にする
  • 単体テストを容易にする

Presentational: 純粋な Viewの定義をするだけのコンポーネント(データは props で受け取る)
Container: ロジック(データ取得処理など)を書いて、presentational component にデータを渡すコンポーネント

使い所

ビューとロジックの関心の分離なら、hooks じゃないの? → そうです.
ただ、hooks に分けるだけだと、以下のような問題が発生し、UIの共通化が難しくなります.
→ hooks と ビュー が1対1になると、ビュー は hooks に依存する.

ロジックは違うが、見た目は一緒みたいなことはたまに出てくる. そういう時に見た目だけ共通化できるのが、Container/Presentational Pattern の魅力.
(大抵は、ロジックも見た目も固有のものだから、1コンポーネント1hooks を作るだけでいい)
ex) Twitter の ツイートの View とか

Container/Presentational Pattern を使った実装例

今回は、Twitter の Tweet 部分を Container/Presentational Patternを使って実装する例を示します.

これから3つのコンポーネントを作成します.

  1. Tweet のリストビューである、TweetList コンポーネント (Presentational Component)
  2. タイムライン(おすすめツイート)を取得する、RecommendTweetList コンポーネント (Container Component)
  3. ユーザーのツイートを取得する、UserTweetList コンポーネント (Container Component)

1のコンポーネントが、使い回したいコンポーネント
2,3のコンポーネントはそれぞれ別の取得ロジックを持っているが、どちらも同じ1のコンポーネントの見た目をしているというケース

TweetList コンポーネント (Presentational Component)

type Tweet = {
  user: {
    name: string;
  };
  content: string;
  image?: string;
  // ...
};

type Props = {
  tweets: Tweet[];
};

const TweetList = ({ tweets }: Props) => {
  return (
    <ul>
      {tweets.map(({ user, content }) => (
        <li>
          <p>{user.name}</p>
          <p>{content}</p>
          {/* ... */}
        </li>
      ))}
    </ul>
  );
};
}

データは props で受け取り、見た目だけに責務を持つコンポーネント (presentational)

RecommendTweetList コンポーネント (Container Component)

const RecommendTweetList = () => {
  const [tweets, setTweets] = useState<Tweet[]>([]);

  useEffect(() => {
    const fetchRecommendTweets = async () => {
      return [
        {
          user: {
            name: "できたてほやほやエンジニア",
          },
          content: "Container/Presentational Pattern のメリットって何?",
        },
      ];
    };

    fetchRecommendTweets().then(setTweets);
  }, []);

  return <TweetList tweets={tweets} />;
};

おすすめのツイートを取得するロジックを書いて、取得することに専念するコンポーネント(container compoent)
見た目は、TweetList なので、そこにデータを渡すだけ.

UserTweetList コンポーネント (Container Component)

const UserTweetList = () => {
  const [tweets, setTweets] = useState<Tweet[]>([]);

  useEffect(() => {
    const fetchUserTweets = async () => {
      return [
        {
          user: {
            name: "できたてほやほやエンジニア",
          },
          content: "Container/Presentational Pattern のメリットって何?",
        },
      ];
    };

    fetchUserTweets().then(setTweets);
  }, []);

  return <TweetList tweets={tweets} />;
};

ユーザーのツイートを取得するロジックを書いて、取得することに専念するコンポーネント (container compoent)
見た目は、TweetList なので、そこにデータを渡すだけ.

Container with hooks/Presentational (最終形態)

ロジックは hooks に閉じ込めて綺麗にする.

const useRecommendTweets = () => {
  const [tweets, setTweets] = useState<Tweet[]>([]);

  useEffect(() => {
    const fetchRecommendTweets = async () => {
      return [
        {
          user: {
            name: "できたてほやほやエンジニア",
          },
          content: "Container/Presentational Pattern のメリットって何?",
        },
      ];
    };

    fetchRecommendTweets().then(setTweets);
  }, []);

  return { tweets };
};

const RecommendTweetList = () => {
  const { tweets } = useRecommendTweets();

  return <TweetList tweets={tweets} />;
};

ロジックを hooks に閉じ込めつつ、View は共通化する綺麗な形になりました.

Container/Presentational は使わずに hooks だけで

const useRecommendTweets = () => {
  const [tweets, setTweets] = useState<Tweet[]>([]);

  useEffect(() => {
    const fetchRecommendTweets = async () => {
      return [
        {
          user: {
            name: "できたてほやほやエンジニア",
          },
          content: "Container/Presentational Pattern のメリットって何?",
        },
      ];
    };

    fetchRecommendTweets().then(setTweets);
  }, []);

  return { tweets };
};

const RecommendTweetList = () => {
  const { tweets } = useRecommendTweets();

  return (
    <ul>
      {tweets.map(({ user, content }) => (
        <li>
          <p>{user.name}</p>
          <p>{content}</p>
          {/* View の中身が続く ... */}
        </li>
      ))}
    </ul>
  );
};

ロジックを hooks に分けることはしたが、View が hooks に依存しているので、使い回すことはできない. → UserTweetList で同じ View をもう一回書くことになる.

メリット

  • ロジック(データ取得処理など)は違うが、UIは一緒の時に、UI部分だけ共通化できる
  • データを props で受け取るので、テストしやすい

(デメリット)

  • 単にロジックを分けたいという理由だけなら、もう一つコンポーネントを作成するより、hooks の方が適してる

5回まで書く予定でしたが、render props pattern と hooks パターンについては、以下の理由により、書かないことにしました.

  • render props pattern は、書き方の差なので、そのパターンを使わないと得られないメリットが少なそうだった
  • hooks はパターンというより、当たり前になってる

Discussion