UIの情報構造に合わせたReact Componentを作る、UI Structureパターン

2023/07/21に公開
1

はじめに

ReactのComponentを、見通しよく開発しやすい形で設計するパターンを整理しました。
自分がUI Structureパターンと呼んでいるこの手法では、UIの情報設計として存在する要素分だけReact Componentを作ります。

ReactのデザインパターンはContainer/Presentationalパターンが有名ですが、Container/Presentationalパターンではロジックに関わるものを担うContainer Componentと見た目に関わるものを担うPresentational Componentにわけ、再利用性を高めています。
しかしUI StructureパターンではContainer Componentを作らずhooksやlogicsで切り出して対応します。

とはいっても、Reactの開発に慣れている方にとっては特別目新しいものではないかもしれません。

UI Structureパターン

UI StructureパターンではReact Componentを「UI要素を表現したもの」と捉えます。
ComponentはDataをもち、Dataを操作でき、ユーザ操作によって状態を変化させます。
ここからComponentが受け取るpropsの種類は以下の3つに限定されます。

  1. Data
  2. Data操作を行うhandler (onCreate, onUpdate, onDelete)
  3. 状態変化に伴うhandler (onActive, onOpen, onClose)

Dataはその要素がUIの情報設計として持っているであろうもののみを含み、他の情報はcontextを用いて渡します。

このパターンを用いるメリット

  • Component構造の見通しが良くなる。構造がUIデザインと一致しているため、まずデザインを見ればコードの構造も把握できる。エンジニアとデザイナーでコンポーネント名を共通して使える。
  • propsがシンプルになる。渡す数が少なくなるだけでなく、どういうものを渡すのかがはっきりする。バケツリレーを防止することができる。

例:Article

以下のような記事を表示することを考えてみましょう。

この記事は以下のような構造でUIが組み上げられています。

UI構造に合わせてReact Componentを切っていきます。
各コンポーネントに渡すDataは、コンポーネント名に一致させるようにします。

▼Article
  index.tsx
  ▼Supplement
    index.tsx
    Info.tsx
    Options.tsx
  ▼Body
    index.tsx
Article/index.tsx
type Props = {
  artile: ArticleT
};

export const Article: React.FC<Props> = ({ article }) => {
  return (
    <div>
      <Supplement supplement={article.supplement} />
      <Body body={article.body} />
    </div>
  );
};

Supplementの中にはブックマークボタンがあり、ブックマークするにはarticle.idが必要です。
しかしSupplementに直接article.idを渡してはいません。SupplementというComponentは article.supplement という情報を扱うためのものだからです。

article.idを渡すためにはcontextを使います。

Article/index.tsx
type Props = {
  artile: ArticleT
};

export const Article: React.FC<Props> = ({ article }) => {
  return (
    <div>
      <ArticleIdContext.Provider value={article.id}>
        <Supplement supplement={article.supplement} />
        <Body body={article.body} />
      </ArticleIdContext.Provider>
    </div>
  );
};

こうすることによって、Componentの階層構造をシンプルに保つとともに、propsを最小限に抑えることができます。

例:Articles

次に記事一覧を表示することを考えてみましょう。
この一覧コンポーネントでは、記事を追加できたり、各記事を削除することができます。

これを表示させるComponentのArticlesは以下のようになるでしょうか。

▼Articles
  index.tsx
  ▼hooks
    useArticles.ts
  ▼ArticleCreator
    index.tsx
  ▼Article
    index.tsx
Articles/index.tsx
export const Articles: React.FC = ({ }) => {
  const { articles, onCreateArticle, onDeleteArticle } = useArticles();
  
  return (
    <div>
      <ArticleCreator onActive={onCreateArticle} />
      <ul>
        {articles.map((article) =>
	  <li key={article.id}>
            <Article article={article} onDelete={() => onDeleteArticle(article.id)} />
	  </li>
        )}
      </ul>
    </div>
  );
};

Articleの追加・削除に必要なハンドラはhooksで切り出し、それを加工しながら子供に渡してあげます。
ArticleCreatorに対してはonActiveで記事作成ハンドラを渡しています。onClickは「状態変化に伴うhandler」とは言えないので、onActiveという名前にしています。

ここで、記事のタイトルを編集できる機能を追加しようとしたらどうすればいいでしょうか。
方法の1つはAPIリクエストを行うonUpdateTitleを作って、Title内でそれを呼ぶことですね。

もう1つの方法はonUpdateArticleを作ることです。
タイトル以外にも編集できるものが複数ある場合にAPI1つで対応しようとすると、こういった形になります。

Articles/index.tsx
export const Articles: React.FC = ({ }) => {
  const { articles, onCreateArticle, onUpdateArticle, onDeleteArticle } = useArticles();
  
  return (
    <div>
      <ArticleCreator onActive={onCreateArticle} />
      <ul>
        {articles.map((article) =>
	  <li key={article.id}>
            <Article
	      article={article}
	      onUpdate={(newArticle) => onUpdateArticle(artcle.id, newArticle)} 
	      onDelete={() => onDeleteArticle(article.id)} />
	  </li>
        )}
      </ul>
    </div>
  );
};
Articles/Article/index.tsx
type Props = {
  article: ArticleT,
  onUpdate: (newArticle: ArticleT) => void,
  onDelete: () => void,
};

export const Article: React.FC<Props> = ({ article, onUpdate, onDelete }) => {
  return (
    <div>
      <Title
        title={article.title}
        onUpdate={(newTitle) => onUpdate({ ...article, title }} />
    </div>
  );
};

どのComponentも「1.Data」「2. Data操作を行うhandler」「3. 状態変化に伴うhandler」以外のpropsを受け取らずに表現できています。

ありそうな議論

再利用性が低いじゃないか

フロントエンドの要素は変化しやすく、かつ場所によって差分が発生しやすいので、再利用するのは慎重に行うべきです。
基本的には再利用可能なComponentは小さい部品に限られると思っていて、それらはデザインシステムとして外部ライブラリに出してしまう方が設計がすっきりします。

Storybookが使いにくいじゃないか

Container/Presentationalパターンに比べてデメリットになっているのはこの部分ですね。ここは悩ましい箇所です。
ただモックサーバで対応することもできるし、見通しが良くなることで高速で開発しやすくなり、かつ組織のスケールアウトに対応しやすいという点で、このデメリットを上回るメリットがあると考えています。

株式会社スタメン

Discussion

Honey32Honey32

この記事の内容、特に「再利用性が低いじゃないか > 再利用するのは慎重に行うべきです。」のところは、私も全面的に賛成です。

蛇足かもしれませんが、React の公式ドキュメントにもそのような旨の説明があるので、よかったら実装方針の議論などでお使いください。

ほとんどの React アプリでは隅から隅までコンポーネントが使われます。つまり、ボタンのような再利用可能なところでのみ使うのではなく、サイドバーやリスト、最終的にはページ本体といった大きなパーツのためにも使うのです。コンポーネントは、1 回しか使わないような UI コードやマークアップであっても、それらを整理するための有用な手段です。

https://ja.react.dev/learn/your-first-component#components-all-the-way-down