より良いコンポーネント設計を見つけたい!「Reactコンポーネント設計分割」選手権!


スタートアップのプロダクト支援をしているGaji-Labo のフロントエンドエンジニアは、日々さまざまなプロダクトに触れ、事業やプロダクトの状況に合ったよりよいコンポーネント設計を模索しています。

社内勉強会やレビューを通じて議論することもあるのですが、話してみると各メンバーそれぞれに「自分の考える良いコンポーネント設計」があることに気づきます。

そしてふと思いました。

各々に考えるコンポーネント設計を戦わせてみたら、より良いコンポーネント設計が見つかるのでは?

突然ですが、フロントエンドエンジニアたちの 天下一 社内武道会「コンポーネント設計分割選手権」の開催です!

コンポーネント設計分割選手権のルール

今回は、この画像のコンポーネントを作成してもらいました。

タイトルコンポーネントと番号付きリストコンポーネントがセットになった画像。リストの子要素は3つあり、タイトルとテキストと背景付き aside がセットになっている。

ルールは以下の通り。

提示された画像を元に「FeatureList」コンポーネントを設計・実装し、Storybook で確認できるようにする。

  • 使用技術:React + TypeScript
  • 評価点:再利用性: コンポーネントが汎用的に使えるものであり再利用性があるか
  • 可読性: 新規開発者であってもすぐに理解できるような単位でコンポーネントが作られているか
  • 関心の分離: コンポーネントの運用がしやすくなるような関心の分離がなされているか

それぞれ5点満点(他にも膨大な数の評価軸があると思いますが今回はこの3つで……!)で評価していきます!

一番手は tsuji 選手!

tsuji さんは、BtoBスタートアップのプロダクト開発に携わっています。

元々はDTP・Webデザイナーを経験した後、フロントエンドエンジニアに転向し、HTML/CSS/JavaScriptを中心にWeb開発を担当されてきました。 UI・UXに興味があり、デザイン・コーディング両面から考えられるデザインエンジニアを目指しているとのこと。

atsuhiro-tsuji

他の方のコンポーネントへの考え方を知ることができる良い機会だなと思っています!

せっかくの機会なので、「私の考える最強のコンポーネント設計」で挑みます!

tsuji さんのコードの特徴としては、

  • Heading が FeatureList コンポーネントの中に含まれている
  • featureList props を受け取り、 FeatureItem 子コンポーネントに渡している

の2点が挙げられます。

import { ComponentPropsWithoutRef } from 'react'
import { Heading } from '../atoms/heading/Heading'
import { FeatureItem } from './FeatureItem'

type Feature = Omit<
  ComponentPropsWithoutRef<typeof FeatureItem>,
  'prefixNumber'
>

type FeatureListProps = {
  featureList: Feature[]
}

export const FeatureList = ({ featureList }: FeatureListProps) => {
  return (
    <section>
      <Heading level={2}>
        Gaji-LaboのUIデザイン
        <br />
        3つの特徴
      </Heading>
      <ul>
        {featureList.map((feature, index) => {
          return (
            <li key={index}>
              <FeatureItem
                prefixNumber={String(index + 1).padStart(2, '0')}
                {...feature}
              />
            </li>
          )
        })}
      </ul>
    </section>
  )
}
atsuhiro-tsuji

> Heading が FeatureList コンポーネントの中に含まれている

今回のお題は静的コンテンツであり更新回数も少ないと予想したので、Section > Heading + 他要素でシンプルにまとめました。

ただ Heading をハードコーディングするのではなく、汎用的な見出しコンポーネント(名称が紛らわしいですが、Heading コンポーネント)にして、h1〜h6 を自由に指定できるようにしています!

> featureList props を受け取り、 FeatureItem 子コンポーネントに渡している

FeatureList コンポーネントの役割は、特徴を描画する FeatureItem 子コンポーネントをまとめる事、と考えました。

ですので、FeatureList コンポーネントは featureList props を受け取って、map で回すだけのシンプルな構成になっています。

特徴を描画するための型情報は FeatureItem 子コンポーネント側にまとめて、ComponentPropsWithoutRef を利用して取り出しています。

この記法なら、props の型情報を親コンポーネント側に  import する必要もなくなりますし、親コンポーネント側で多少のカスタマイズもできますので重宝しています!

実際にコンポーネントを使うStorybook のコードがこちらです。

const Template: Story<ComponentPropsWithoutRef<typeof FeatureList>> = (
  args
) => {
  return <FeatureList {...args} />
}

export const Base = Template.bind({})
Base.args = {
  featureList: [
    {
      title: (
        <>
          UIデザインを作るための
          <br />
          最適な体験の設計
        </>
      ),
      description: (
        <>
          プロダクトの課題や問題に対して、分析と検証を重ねて、具体的な画面のビジュアルとインタラクションを設計します。
          <br />
          プロダクトでユーザーが接するUIや機能は、ユーザビリティと操作性を達成するデザインで構築します。
        </>
      ),
      subTitle: 'UIデザインプロトタイピング',
      subDescription: (
        <>
          画面設計/構成、ワイヤーフレームからUIデザインへビジュアライズを行います。ユーザビリティを考慮し機能性と操作性を達成するUIデザインと、操作感を確かめていただけるプロトタイピングモックをご提案します。
        </>
      ),
      steps: [
        { title: 'ヒアリング(オンライン)' },
        { title: '新規画面のUIデザイン' },
        { title: 'プロトタイプ作成' },
      ],
    },
  ],
}

※詳細なコードはこちら: Codesandbox / Storybook

審査員の反応

選手として参加していない Gaji-Labo のフロントエンドエンジニアが審査員としてコメントします。

YoshizawaAsato

YoshizawaAsato

FeatureListコンポーネントは汎用的なものでも無いと思うので、最低限の柔軟性だけ持たせて他はガッチリ決めているところが良いと思いました!

引数を変えるだけで、例えば「Gaji-Labo流 3つの仕事の流儀」とかそんな風に簡単に流用できるのが良いと思います!ComponentPropsをimportせずに持ってくるのも私は好きです。

yokota

yokota

見出しまで含まれているのでこのコンポーネントひとつでセクションを再現できるのがカタイと思いました

tsuji さんの得点は…

  • 再利用性:5点
  • 可読性:4点
  • 関心の分離:3点

合計12点となりました!

FeatureList というコンポーネントの用途と使用場面を理解した実装で、これ1つでセクションの表現ができるという点がポイントとなりました。

二番手は hchaki  選手!

hchaki さんは Next.js + TypeScript を使ったプロダクトに関わっています。

元々はデザイン畑の出身で、気持ちのいいアニメーションやインタラクティブな表現を丁寧に手掛けます。

hchaki

コンポーネントに捧げる愛なら誰にも負けません!

見せましょう!読みやすく、変更に強い、真のコードを!

hchaki さんのコードの特徴としては、

  • Heading は FeatureList コンポーネントの中に含めていない
  • リスト項目自体は他コンポーネントに切り出されておらず、青背景部分が Detail 子コンポーネントに切り出されている

の2点が挙げられます。

import { ReactElement, ReactNode } from 'react'

import { Detail } from './Detail'
import type { Props as DetailProps } from './Detail'
interface Item {
  title: ReactNode
  description: ReactNode
  detail: DetailProps
}

export interface Props {
  items: Item[]
}

export const FeatureList = ({ items }: Props): ReactElement => {
  return (
    <ul>
      {items.map(({ title, description, detail }, index) => {
        return (
          <li key={index + 1}>
            <dl>
              <dt>
                <h2>
                  {String(index + 1).padStart(2, '0')}.<br />
                  {title}
                </h2>
              </dt>
              <dd>
                {description}
                <Detail {...detail} />
              </dd>
            </dl>
          </li>
        )
      })}
    </ul>
  )
}
hchaki

> Heading は FeatureList コンポーネントの中に含めていない

List コンポーネントは、配列をうけとって、リストの要素として処理する役割だけを持たせたいと考えます。そのため、Heading は List コンポーネントに含めていません。

別ページでも使いたくなったときには、Heading と FeatureList を内包した、ラッパーコンポーネントを作成するでしょう。ただ、現状では不要かなあと判断しました。

> リスト項目自体は他コンポーネントに切り出されておらず、青背景部分が Detail 子コンポーネントに切り出されている

リスト項目は、単純な表示のため、切り出さないことにしました。

ただ、ここは反省点があり、ナンバリングを index から行うのは、ロジックを複雑にした割に利が少ないですね。素直に props から渡すほうがシンプルだったかも。(他2人のコードを見てハッとしました)

Detail のステップごとになにかを表示するという機能は、使い回す可能性がそれなりに想定できるので、コンポーネントに切り出しています。実際に他ページ使い回すことになったら、Detail コンポーネントは汎用コンポーネントのディレクトリに移動するのが良いでしょうが今はまだ早いです。

実際にコンポーネントを使うStorybook のコードがこちらです。

export const Default = (props: Props) => <FeatureList {...props} />

Default.args = {
  items: [
    {
      title: (
        <>
          UIデザインを作るための
          <br />
          最適な体験の設計
        </>
      ),
      description: (
        <>
          プロダクトの課題や問題に対して、分析と検証を重ねて、具体的な画面のビジュアルとインタラクションを設計します。
          <br />
          プロダクトでユーザーが接するUIや機能は、ユーザビリティと操作性を達成するデザインで構築します。
        </>
      ),
      detail: {
        title: 'UIデザインプロトタイピング',
        description: (
          <>
            画面設計/構成、ワイヤーフレームからUIデザインへビジュアライズを行います。ユーザビリティを考慮し機能性と操作性を達成するUIデザインと、操作感を確かめていただけるプロトタイピングモックをご提案します。
          </>
        ),
        steps: [
          'ヒアリング(オンライン)',
          '新規画面のUIデザイン',
          'プロトタイプ作成',
        ],
      },
    },
  ],
}

※詳細なコードはこちら: Codesandbox / Storybook

審査員の反応

YoshizawaAsato

YoshizawaAsato

Headingが入っていないのはList感があって良いですね!

mk-0A0

mk-0A0

ナンバリングがindexになっているとリストのどこかを削除しても自動的に番号が変わると思うので、たしかに変更に強いかも。

yokota

yokota

この時点ですでにDetail コンポーネントが切り出されているのが分割の粒度として好きです

hchaki さんの得点は…

  • 再利用性:4点
  • 可読性:5点
  • 関心の分離:3点

なんと、一番手の tsuji さんと同点となる12点でした!

コンポーネントの命名と実態が一致している実装で、一目見てこのコンポーネントが何であってどう使うのかが理解しやすい点がポイントとなりました。

最後は mach3  選手です!

mach3 さんも hchaki さんと同じく Next.js + TypeScript での開発に携わっています。

デザインからバックエンドまで網羅的にこなすマルチデザイナーとして長く活動してきた経験があり、 JavaScript でのモダンな開発に関心のあるフロントエンドエンジニアです。

hisashi-oikawa

Webの世界で過ごしてきた長さなら負けません。

しっかりと漬け込んだ秘伝のコンポーネント設計をお届けします。

mach3 さんのコードの特徴としては、

  • FeatureList 自体は簡潔!
  • FeatureList の中で children を受け取り、子コンポーネントを組み合わせて作っている

の2点が挙げられます。

import { ReactNode } from "react";

interface FeatureListProps {
  children: ReactNode
}

export function FeatureList (props: FeatureListProps): JSX.Element {
  const { children } = props;

  return (
    <div className="FeatureList">
      {children}
    </div>
  )
}
hisashi-oikawa

> FeatureList 自体は簡潔!

主に設計で気をつけているポイントは「1つのコンポーネントに多くの性格や役割を持たせない」ことです。できるだけ単一機能で完結するシンプルなコンポーネントを作るよう、心がけています。

ここでは「レイアウトを組む」という役割をもたせた `FeatureList` と「情報を表示させる」ための `FeatureItem` に責務を分離し、メンテナンスや拡張がしやすいように配慮をしています。

> FeatureList の中で children を受け取り、子コンポーネントを組み合わせて作っている

Reactコンポーネントも突き詰めれば実態はHTMLElementなので、HTMLで記述するかのようにコンポーネントを組み立てられる設計が実態を正しく表している感じがあって個人的には好きです。レイアウトを組むもの、データを表示するもの、それらをコントロールするものなど、コンポーネント個々の役割を明確にさせて、 children を介してヒエラルキーでもって表現していくのが正しいあり方ではないかと考えて設計をしています。

実際にコンポーネントを使うStorybook のコードがこちらです。

import { FeatureItem } from "./FeatureItem"
import { FeatureItemCard } from "./FeatureItemCard"
import { FeatureList } from "./FeatureList"

export const Default = ():JSX.Element => {
  return (
    <FeatureList>
      <FeatureItem
        numberName="01."
        title={<>UIデザインを作るための<br />最適な体験の設計</>}
      >
        <p>プロダクトの課題や問題に対して、分析と検証を重ねて、具体的な画面のビジュアルとインタラクションを設計します。</p>
        <p>プロダクトでユーザーが接するUIや機能は、ユーザビリティと操作性を達成するデザインで構築します。</p>
        <FeatureItemCard
          title="UIデザインプロトタイピング"
          description="画面設計/構成、ワイヤーフレームからUIデザインへビジュアライズを行います。ユーザビリティを考慮し機能性と操作性を達成するUIデザインと、操作感を確かめていただけるプロトタイピングモックをご提案します。"
          steps={[
            {
              label: "Step 1",
              content: "ヒアリング(オンライン)" 
            },
            {
              label: "Step 2",
              content: "新規画面のUIデザイン"
            },
            {
              label: "Step 3",
              content: "プロトタイプ作成"
            }
          ]}
        />
      </FeatureItem>
    </FeatureList>
  )
}

※詳細なコードはこちら: Codesandbox / Storybook

審査員の反応

YoshizawaAsato

YoshizawaAsato

FeatureListでレイアウトだけ決めるようにしているのが良いと思いました!

yokota

yokota

シンプルかつ拡張性もあってどんな子要素がきても対応できそうだと思いました!

thkt

thkt

責務の分離の仕方がshadcn/ui的で、モダンな作りになっていてオシャレだなと思いました。好きです!

mach3 さんの得点は…

  • 再利用性:4点
  • 可読性:3点
  • 関心の分離:5点

コンポーネントの分割単位が先の二人よりも細かく、単一責務の原則に則った実装により再利用性と拡張性を担保された実装であることがポイントとなりました。

審査員が優勝者を決められない。

なんと、3人とも同点……!

「mach3 さんのコードがコンポーネントごとの役割がしっかりしていていいのでは?」

「再利用性を考えると tsuji さんのコードがタイトルまで含まれていてよさそう」

「hchakiさんのコードが二人の良いところを取ってバランスがいいな〜」

……と、審査員の間で意見の相違があり、優勝者が決まりませんでした

tsuji さんのコードは再利用性があり、コンポーネントとして使いやすい設計になっているなと感じました。「再利用性に長けているで賞」といったところでしょうか。

hchaki さんは「可読性が高いで賞」をあげたいです。

リスト単位でコンポーネントが作られているため、何をするコンポーネントかわかりやすいと感じました。

mach3 さんはコンポーネントごとの役割がはっきりしており分離しているため、メンテナンスがしやすいと感じました。「関心の分離が意識されているで賞」をあげたいところです。

優勝者を決めると言って参加してもらったのに申し訳ありません……

コンポーネント設計の正解はひとつではなく、評価の軸をどこに置くかでがいくらでも変わってきます。大事なのは「このコンポーネントは何に重きを置くべきか」を状況に合わせて設計を使い分けられるようになることかなと改めて感じました。

普段からプロダクトや開発チームの状況に合わせた設計を考えているので、「これが正解」というものはないのは分かっていたつもりでしたし、当たり前といえば当たり前の結果ではあるのですが、やはりコンポーネント設計は奥が深いなと改めて感じました。

最後に。

「自分ならこうするよ!」という、フロントエンドエンジニアの方は(転職しなくても良いので)カジュアル面談でお話しましょう~

司会の semigura でした!

Gaji-Labo フロントエンドエンジニア向けご案内資料

Gaji-Labo は新規事業やサービス開発に取り組む、事業会社・スタートアップへの支援を行っています。

弊社では、Next.js を用いた Web アプリケーションのフロントエンド開発をリードするフロントエンドエンジニアを募集しています!さまざまなプロダクトやチームに関わりながら、一緒に成長を体験しませんか?

もちろん、一緒にお仕事をしてくださるパートナーさんも随時募集中です。まずはお気軽に声をかけてください!

求人応募してみる!


投稿者 Ishigaki Shotaro

未経験から Gaji-Labo に入社。現在は React/TypeScript/Next.js の案件で MUI を使ったコンポーネント組み込みを担当。プロジェクトチームのリードとして共に組み込み作業をしているメンバーの進行管理も行っています。休日はだいたい家で音楽を聴いており、たまにライブに出かけています。