良いコンポーネントを作るために気をつけている3つのこと

はじめに

 こんにちは、コミューンでフロントエンドエンジニアをしている根岸です。

 この記事では自分がフロントエンドのコンポーネントを作るときに気をつけていることを紹介します。

良いコンポーネントとは

 そもそも良いコンポーネント、良いコードとは何でしょうか?

 私は プログラマが知るべき97のこと美はシンプルさに宿る という記事の下の一文に大きな影響を受けています。

特に重要なのが「シンプルである」ということです。アプリケーションやシステムが全体としてどれほど複雑であっても、個々の部分を取り出してみると、全てシンプルになっています。

 重要なのはどんな複雑なアプリケーションでも、一部を取り出すとシンプルになっているということです。 1つのエンドポイント、1つファイル、1つの関数のようにどんな粒度で取り出したとしてもシンプルになっているコードが良いコードだと私は考えています。 これはフロントエンド開発におけるコンポーネントに対しても同じことがいえます。

 もちろん常にそのようなシンプルなコードを書けるわけではありません。 しかし、どんな粒度でもシンプルに感じられるようなコードを目指して日々コーディングをしています。

良いコンポーネントを作るためのポイント

 私がコンポーネントを書くときに特に意識している3つのポイントについて、下のcommmuneの投稿画面で使っているPostsDetailコンポーネントを例にとって説明します。

1. コンポーネントを要素ごとに過不足なく分割する

 単一の責務を持ったコンポーネントは変更に強く保守性の高いコンポーネントになります。 デザインから要素を抽出して、その要素ごとにコンポーネントを区切ると単一の責務をもったコンポーネントをつくることができます。

 PostsDetailのユーザ情報を表示している部分について、要素を抽出してそれぞれの要素をコンポーネントに分割してみます。

 この部分にどんな要素があるかを考えると下のような5つの要素が抽出できます。

  1. アイコン画像を表示する
  2. ユーザ名を表示する
  3. ユーザIDを表示する
  4. 投稿日時を表示する
  5. アイコン画像を左、ユーザ名・アカウント名を右上、投稿日時を右下に配置するレイアウト

 これらの要素を愚直にそれぞれコンポーネントに切り出します。

// 1. アイコン画像
const Icon = ({ ... }) => { ... }
// 2. ユーザ名
const UserName = ({ ... }) => { ... }
// 3. ユーザID
const UserId = ({ ... }) => { ... }
// 4. 投稿日時
const DateTime = ({ ... }) => { ... }
// 5. レイアウト
const Layout = ({ ... }) => { ... }

 切り出したコンポーネントを組み合わせると、下のようなコードになります。

const UserInfo = ({ iconSrc, name, id, postDateTimeText }) => {
  return (
    <UserLayout
      leftElement={<Icon src={iconSrc} />}
      upperRightElement={
        <>
          <UserName>{name}</UserName>
          <UserId>@{id}</UserId>
        </>
      }
      bottomRightElement={<DateTime>{postDateTimeText}</DateTime>}
    />
  )
}

 このように要素一つ一つをコンポーネントに切り出して一つのコンポーネントを作り上げると変更に強いコンポーネントになります。

 例えばIconを丸型にトリミングしたいという変更があったとき、影響範囲はIconコンポーネント内に留まります。他のコンポーネントは全く影響を受けないため、Iconコンポーネント の内部を考えるだけで変更に対応できます。そのため変更に対応しやすい構造になります。

 一見すると面倒ですが1つ1つの要素ごとにコンポーネントに分割することで、責務が綺麗に分割され、保守性の高いコンポーネントを作ることができます。

2. コンポーネントの抽象度を揃える

 コンポーネントを分割することに加えて、コンポーネントがレイヤーに分かれて抽象度が揃えられていると簡潔で理解しやすいコードになります。

 PostsDetailコンポーネントについて、どのように分割できるか考えてみます。 まず、PostsDetailコンポーネント全体を見ると

  • ユーザ情報や戻るボタンの含まれるヘッダー
  • 投稿の内容が表示されているメインコンテンツとなる部分
  • コメントの入力欄があるフッター

の3つに分けると抽象度をそろえることができそうです。

 さらにヘッダーの部分に着目します。 1で扱ったユーザ情報を扱うUserInfoコンポーネントと同じ抽象度で他の要素を分割できないか考えると

  • 戻るボタン
  • ユーザ情報(UserInfo)
  • メニュー
  • 戻るボタン・ユーザ情報・メニューの3つの要素を配置するレイアウト

の4つに分割できそうです。

分割したコンポーネントをそれぞれコードに落とし込むと以下のようになります。

const Header = () => {
  return (
    <HeaderLayout>
      <BackButton />
      <UserInfo />
      <Menu />
    </HeaderLayout>
  )
}

const Main =  ({ ... }) => { ... }

const Footer =  ({ ... }) => { ... }

const PostDetail = () => {
  return (
    <>
      <Header />
      <Main />
      <Footer />
    </>
  )
}

 上のコードをみると、見通しがよく、PostDetailとHeaderの構造がひと目でわかるようなコードになっていると思います。 どのコードがどのデザインに対応しているのか探すことも容易になります。

 もしHeaderやUserInforがコンポーネントに分割されていなかったら、PostDetailは下のようになります。

const PostDetail = () => {
  return (
    <>
      <HeaderLayout>
        <BackButton />
        <UserLayout
          leftElement={<Icon src={...} />}
          upperRightElement={
            <>
              <UserName>...</UserName>
              <UserId>...</UserId>
            </>
          }
          bottomRightElement={<DateTime>{...}</DateTime>}
        />

        <Menu />
      </HeaderLayout>
      <Main />
      <Footer />
    </>
  )
}

 PostDetailの中で使われているコンポーネントの抽象度が揃っておらず、見通しが非常に悪いです。 とくにヘッダーの詳細が露出しており、PostDetailがどんな構造をしているのかが分かりづらくなってしまいます。

3. 利用する側を意識せずにコンポーネントを作る

 利用する側を意識せずに作られたコンポーネントは疎結合で保守しやすいコンポーネントになります。

 PostsDetailコンポーネントのコメント入力欄を例にとって考えてみます。 青い線で囲まれたコメントの入力欄全体を示すコンポーネントをCommentForm、赤い線で囲まれたテキストの入力部分を示すコンポーネントをTextInputFieldという名前で定義したとします。

 ここでTextInputFieldに入力された値をCommentFormのstateに渡したいとき、どんなpropsを定義すればよいでしょうか?

 例えば下のように useState の返り値 と同じ名前のpropsを作ったとします。

// 悪い例
const CommentFrom = () => {
  const [text, setText] = useState('')
  return (
    ...
    <TextInputField setText={setText} /> // CommentFormのsetTextに引きずられたpropsになっている
    ...
  )
}

 この例のTextInputFieldCommentFrom で利用している関数名の影響を受けてしまっており、利用する側の意識したコンポーネントになってしまっています。

 利用する側を意識して実装してしまうと、利用する側の変更の影響を受けやすくなります。

 例えばsetTextの名前を変えると TextInputFieldのprops名も変えないといけなくなり、保守するのが大変になります。 また、setText というprops名は一般的ではなく、 CommentFrom以外のコンポーネントで利用するのは違和感が生じます。

 props名を setText からonChange という名前に変えると下のようになります。

// 良い例
const CommentFrom = () => {
  const [text, setText] = useState('')
  return (
    ...
    <TextInputField onChange={setText} />
    ...
  )
}

 props名を変更したことで TextInputFieldCommentFrom の影響を受けなくなりました。 また、TextInputField` が他のコンポーネントで使われたとしても違和感のないpropsになっており、再利用性・保守性が高くなりました。

最後に

コミューンではフロントエンドエンジニアを募集中です!

コミューンの開発に興味を持った方はぜひエントリーしてみてください!

commmune-careers.studio.site

https://commmune.connpass.com/event/248046/commmune.connpass.com

https://commmune.connpass.com/event/249425/commmune.connpass.com