TWBMT

技術的な記事や覚書について書いていきます。その内、自作サイトとかに技術記事をまとめたい。

Container / Presentational Pattern について

Container / Presentational パターンはコンポーネント設計や責務分担を考える時に役立つデザインパターンだと思います。
フロントエンド始めたての方なんかは是非学んで欲しいパターンだなぁと。

なので、最初に自分が理解する為に呼んだ記事や勘所等をまとめたいと思います。

Container / Presentational パターンとは

概要

コンポーネントを以下の2種類に分類して実装するパターンのこと

  1. Container Component
    振る舞いに関する関心を集めたコンポーネント

  2. Presentational Component
    見た目に関する関心を集めたコンポーネント

...と言われても、こういう概念はサンプルコードが無いと意味がわからないと思います。
なので、イメージを掴みやすいように参考例を作成してみました。

サンプルコード

個人的に一番わかり易い事例はモーダルダイアログだと思います。

仮に「外部APIから取得したデータを表示する」「送信ボタンを押した際にAPIへ何らかの値を送信する」 という処理を持ったモーダルダイアログがあるとします。
その時、もしもモーダルダイアログに全ての処理を実装すると以下の様になってしまいます。

const ModalDIalogComponent = () => {
  const [content, setContent] = useState();
  useEffect(() => {
    /*
      外部APIから表示する値を取得する
      const val: any = "何らかの値";
      setContent(val)
     */
  },[])
  const onSubmit = useCallback(() => {
    /*
     * APIの送信処理など
     */
  },[])

  return (
    <div  className={"モーダルダイアログに関するCSSクラス"}>
      <span>
        {props.content}
      </span>
      <button onClick={onSubmit}>送信</button>
    </div>
  )
}

かなりツッコミどころのある事例ですが、これが「見た目に関するロジックと振る舞いに関するロジックが混在している状態」です。

今、このコンポーネントには「モーダルダイアログのスタイリング」「データを取得する処理」「APIを送信する処理」などが記述されています。
API を送信する処理は見た目に関係ありませんし、データを取得する処理自体は直接見た目に関係しているわけではありません。

なので、このコンポーネントから見た目に関するロジック以外を取り出し、親コンポーネントに処理を委譲してみます。

type Props = {
  content: string;
  onSubmit: () => void;
}
const ModalDIalogComponent = (props:Props) => {
  return (
    <div className={"モーダルダイアログに関するCSSクラス"}>
      <span>
        {props.content}
      </span>
      <button onClick={props.onSubmit}>送信</button>
    </div>
  )
}

const PageComponent = () => {
  const [content, setContent] = useState();
  useEffect(() => {
    /*
      APIやLocalStorageから表示する値を取得する
      const val: any = "何らかの値";
      setContent(val)
     */
  },[])
  const onSubmit = useCallback(() => {
    /*
     * APIの送信処理など
     */
  },[])

  return (
    < ModalDIalogComponent content={content} onSubmit={onSubmit}></ModalDIalogComponent>
  )
}

モーダルダイアログ側のコンポーネントは親コンポーネントから受け取った値を表示し、受け取ったコールバック関数を実行するだけのコンポーネントになりました。

元の状態の場合、別な API から値を取得したくなった時、類似のコンポーネントを作るか、モーダルダイアログを拡張する必要があります。
しかし、親コンポーネントから受け取る props に応じて振る舞いを変更できる様にすることで、そのまま再利用することが出来るようになりました。

こうして「見た目に関するロジック」「振る舞いに関するロジック」の観点でコンポーネントを分割するパターンをContainer / Presentational パターンと言います。

理解する為の勘所

僕がこのパターンを理解する為に必要だった勘所が2個ありまして、それぞれまとめていきます。

Pure Container よりも Pure Presentational を意識する

完全にロジックしか持たないピュアな Container Component よりも 完全に見た目に関するロジックしか持たないピュアな Presentational Component を目指してみると理解が捗ると思います。

二分割する一方のみに注視した方がわかりやすいですし、ロジックの複雑さ的にも Presentational 側の方がわかりやすいです。

なので、最初は大きい粒度でコンポーネントを作り「ここ共通化出来るかもな〜」「流石にコンポーネントが大きくなってきたな〜」と感じ始めた時にコンポーネントを切り分けていく内に、自然と Container / Presentational な関係になります。

また、最初からキレイに分割することは難度が高い上に、早過ぎる最適化になりやすく、無駄にもなりかねません。
デザインや仕様が固まりきっていない内は無理にチャレンジしないほうが得策かなぁと思います。

とどのつまり「責務分担」

そもそも「見た目と振る舞いに関する関心をわけること」とはすなわち「責務分担を考えること」ということです。
このパターンの名付け親の Dun 氏も「Reactを長くやっているのであればおそらく既に発見していることでしょう」と述べています。
なので、恐らく多くの開発者がコンポーネントを分割するにあたって責務を考えた時に自然に辿り着く様なことだと思います。

というわけで、そもそもそのコンポーネントが行うべき処理や責任範囲を考えて分類してみよう、という観点でもう一度見つめ直してみるとしっくり来ると思います。

で、何が嬉しいの?

ざっくり箇条書きにすると、

  • 可読性の向上
  • テスト容易性の向上
  • Storybook の導入のしやすさ
  • etc...

といった感じかなぁと思います。

とどのつまり「責務を考えよう」というお話なので、コーディングスキルの向上やテスト容易性の向上に繋がるのかなぁと。 後は、特に Storybook なんかを使いたい時は、チームの共通認識として持っておかないと破綻しかねないかなぁと思います。

あとがき

というわけで、Container / Presentational Pattern についてまとめました。
少し古いかもしれませんが、自分の理解の言語化+ちょこちょこReact教えて!みたいな話されるので説明用の資料になれば良いなぁという感じですね。

後、書いていて思ったことですが、フロントエンドに入門して一番興味が湧くことの一つがコンポーネントの分け方だと思うので、そこに責務分担を絡めて学べたらゆくゆくの成長に繋がったりするのかなぁと。
なので、やっぱり基本的な考え方として抑えておくことは今からでも結構、有用なのかなぁと思います。

余談:初出

Redux 作者の Dan Abramov 氏の記事が初出です。(調べる限りは間違いないはず。)

medium.com

著者本人は「Hooks の登場によって必ずしもこのパターンが最適ではなくなった。コードベースで自然だとわかった時は便利なパターンではある。」(意訳)としています。
また、記事の締めで「このパターンを dogma (教義) としないで」とも述べていて、この2つのコンポーネントは必ずキレイに分かれるものではなく、分割が難しい場合もあるとつけくわえています。

参考リンク

コンテナ・プレゼンテーションパターン|フロントエンドのデザインパターン

Container/Presentational Pattern