共通UIコンポーネントで速くクオリティの高いプロダクトを開発をしたい。

目次

  • はじめに
  • 共通UIコンポーネントとは
  • 共通UIコンポーネントを作り、運用することのメリット
    • デザインの一貫性やクオリティが保たれる
    • 開発コスト&デザインコストを削減できる
  • 実装時に考慮すると良いこと
    1. スタイルを適切なpropsで操作可能か
    2. コンポーネントのトップの要素にmarginをつけない
    3. 親や子の要素をお互いが知っている前提の実装をしない
  • おわりに

はじめに

はじめまして、株式会社スタメンの神尾です。 普段はフロントエンドにReact、バックエンドにRuby on Railsを用いて、弊社が運営しているエンゲージメントプラットフォームTUNAGの開発をしています。 TUNAGには多種多様な機能がありますが、普段のReactを用いたフロントエンド開発において、それら特定の機能に紐づかず共通で使用できるコンポーネントを実装したい時が多々あります。 今回は、そのようなReactコンポーネントを共通UIコンポーネントと定義し、共通UIコンポーネントの説明、メリット、実装時に考慮すると良いことを紹介していきたいと思います。

共通UIコンポーネントとは

共通UIコンポーネント」とは、ドメインの知識を持たないコンポーネントのことで、具体的には Button, Modal, Pulldown, Tab, Checkbox 等があります。 これらを提供するライブラリとして有名なものは、以下のようなライブラリがあります。

基本的には、これらのライブラリが提供しているReactコンポーネントが、この記事で言う共通UIコンポーネントです。

例 ) 共通UIコンポーネントとして汎用的なボタンを作成し、様々な機能で使用できるようにしている(投稿、制度、組織、チャットはTUNAGの機能名です)

共通UIコンポーネントを作り、運用することのメリット

  1. デザインの一貫性やクオリティを保つことができる
  2. 開発コストとデザインコストを削減できる

デザインの一貫性やクオリティを保つことができる

共通UIコンポーネントがあることで、特定の機能のコンポーネント内で当てるスタイルを減らすことが出来ます。 そうすることで、デザインの軽微な差異を無くすことが出来るので、一貫性やクオリティを保つことができます。 次に、共通UIコンポーネントがない場合を考えることでメリットが伝わりやすいと思うので、具体例を挙げて考えてみます。

共通UIコンポーネントがない場合

共通UIコンポーネントとしてボタンが無い場合、各機能のコンポーネント内でボタンを実装することになります。 上の画像で言うと「投稿」のコンポーネントの中でボタンを実装し、また別の「制度」でもボタンを実装する等、機能毎にボタンを実装することになります。 そのように色々な機能でUIコンポーネントが実装されると次のようなことが起こります。

ボタンにはborder-radiusが当てられることが多くあると思いますが、各機能ごとにボタンを実装する場合border-radiusもそれぞれのボタンで実装されます。 それらの実装は、それぞれ時期も実装者もデザイナーも異なる可能性が高いので、その都度デザイナーがスタイルを決め、実装することになります。 そのため、この機能の時のボタンのborder-radiusは4px、また別のボタンのborder-radiusは6pxなど、微妙な違いが出てしまう可能性があります。

この問題は、デザインルールがあり、かつ浸透していれば、避けられる問題ではありますが、エンジニアが実装する際に誤ったスタイルを当ててしてしまうことや、毎回実装やデザインの確認をするコストを考えると、同じ機能を持ったボタンを複数回実装することは避けた方が良いと考えられます。

さらに、色々な場所で同じボタンが実装されていると共通でスタイルを変更したくなった時にとても厄介なことになります。例えば「ボタンのbackground-colorを変更したい」となった時、各機能のコンポーネント内でボタンを実装している場合、どのようにbackground-colorを変更すれば良いでしょうか。

それは「ひとつひとつ変更していく」しか方法はありません。

「ボタンは投稿で使ってたから変更して、制度でも使ってたから、これも変更して、、」という風にひとつひとつ変更をしていたら「組織」でボタンを使っていたことを失念する可能性は十分あります。 そのようなことが続いていくと「デザインの一貫性」は崩れていきます。

例 ) 各機能で実装していると変更漏れなどでデザインの一貫性は崩れてしまう

そこで共通UIコンポーネントで、これらの問題を解決します。

どの機能でも共通で使用できるボタンがあることで、ボタンのbackground-colorを変更したくなった時は、共通UIコンポーネントのbackground-colorだけを変更すれば、投稿、制度、組織、チャットの全てのボタンも一緒に変更されるため、変更漏れが起きることはありません。

このように共通UIコンポーネントを上手く使うことで「デザインの一貫性」を高めることに繋がります。

例 ) 共通UIコンポーネントとしてのボタンがあれば、変更漏れを無くすことができる。

開発コストとデザインコストを削減できる

共通UIコンポーネントがあることによって、開発コストやデザインコストが削減できます。 理由は以下のようなものです。どれも大きなメリットだと思います。

  1. CSSを書く量が減る。
    • 共通UIコンポーネントに既にスタイルが当たっているため
  2. デザインレビューする箇所が減る。
    • 共通UIコンポーネントはスタイルも共通のため
  3. 「ここのpaddingどうしますか」のような細かいスタイルのコミュニケーションが減る。
    • 改めて実装する必要がないので確認する必要がないため
  4. 使いまわせるコンポーネントがあるので、一からデザインを作成する必要がない。
    • 同じ機能を持った似たようなデザインを考える必要がないため

以上が自分が感じている共通UIコンポーネントのメリットになります。 ここからは、実装について見ていきたいと思います。

実装時に考慮すると良いこと

ここからは共通UIコンポーネントを実装する時に考慮した方が良いことについて書いていきます。 共通UIコンポーネントは実装時に考慮するべきことが多いので、気づいたら汎用的でないコンポーネントになってしまうことが多くあると感じています。 言われれば「そりゃそうだ」と思うこともあると思いますが、今回は3点紹介させていただきます。

スタイルを適切なpropsで操作可能か

共通UIコンポーネントと言っても「どこで何のために表示するか」が異なれば、当てるスタイル全てが共通になるわけではありません。 そのような時に備えて、柔軟にスタイルを変更できるようにしておくことは、より良い共通UIコンポーネントの実装には重要なことです。

もちろん、そのような違いが多すぎると共通のスタイルが少なくなってしまい、共通UIコンポーネントの意味も薄れ、上記のメリットで上げた「デザインの一貫性」が減ってしまうので気をつけなければならないと思います。 その点に関しては、デザイナーとエンジニアで「どこまでスタイルの違いを許容するか」を話し合って決める必要があると思いますが、一旦ここでは「使う場所によって、ここのスタイルは変えたい」となった場合を考えます。

例えば、以下の画像のようなタブコンポーネントを実装したとします。タブ全体には灰色のborder-bottomがあり、選択されているタブには水色のborder-bottomがあるデザインとなっています。

このコンポーネントを作成した時点では、このスタイルでしたが、別の機能で使用する際にデザインの観点からborder-bottomを表示しないようになったとします。

このような時はisDisplayBorderBottomといったボーダーを表示するかどうかのフラグのpropsで表示・非表示を分けられるように実装しておき、呼び出し側から、このフラグを操作できるようにしておけば、より汎用的な共通UIコンポーネントを作ることができます。 この時にis機能名 のようなフラグにしてしまうと、また別の機能で使用する時に、is機能名2のようなフラグを増やさなければいけないので、避けた方が良いです。

また、例として挙げたisDisplayBorderBottomでも「ボーダーの表示・非表示」の対応はできますが、ボーダーの色を変更したいとなった時に対応できません。 そのような変更の可能性に備えて、以下のようなpropsにすると、さらに汎用的なコンポーネントになります。

borderBottom: 'mainColor' | 'subColor' | 'none'

このように、propsひとつで対応できる幅が大きく変わるので、適切なpropsは何かということを意識することが大事になります。

コンポーネントのトップの要素にmarginをつけない

コンポーネントのトップの要素にmarginをつけると、使い回しづらいコンポーネントになります。なぜ使い回しづらくなるのかを具体例で考えます。

  1. 下記の①では入力フォームとボタンの間にmarginが16px必要だったのでボタンコンポーネントにmargin-leftを16pxつけることにしました。
  2. 今回は入力フォームの右にflexboxを使って配置したらデザインが実装できました。
  3. 次に②のデザインを実装することになりました。プルダウンとボタンの間は8pxです。
  4. ボタンは同じなので、①で作ったボタンコンポーネントを使い回そうとしましたが、margin-left: 16pxが付いているのでプルダウンとボタンの間が8pxに出来ません。
  5. どうしよう、、、

=> このようにコンポーネント自身にmarginが付いていると、他で使う時に邪魔になってしまうことがあります。そもそもmarginはボタンの要素ではなくボタンと他のコンポーネントの間にある空間なのでボタンコンポーネントが持つべきスタイルではありません。

marginはボタンを呼び出しているところで当てるようにすると、使い回しやすいコンポーネントになります。

// もろもろ省略
return (
  <Wrapper>
    <Input />
    <PrimaryButtonContainer>
      <PrimaryButton onClick={handleOnClick} />
    </PrimaryButtonContainer>
  </Wrapper>
)

// styled-components
const Wrapper = styled.div`
  display: flex;
`
const PrimaryButtonContainer = styled.div`
  margin-left: 16px;
`

親や子の要素をお互いが知っている前提の実装をしないこと

コンポーネント自身が「どのようなレイアウトで表示されるか」「親要素が何か」などを知っている前提の実装になっていると使い回しづらいコンポーネントになります。

以下は、UserCardList(親)とUserCard(子)がお互いの要素を知っている例です。 「親や子の要素をお互いが知っている」とはどういうことなのかを説明します。

UserCardListはUlなので、その子はLiでなければなりませんが、以下のUserCardListコンポーネント内だけでは子要素のUserCardがLiで作られたコンポーネントかどうかを判別できません。 今回の例ではUserCardはLiなので問題はありませんが、その「問題ない」かどうかを判別するには、親(UserCardList)が子(UserCard)がLiで作られたコンポーネントであると知っている必要があります。 反対に、Liの親はUlでなければならないのでUserCardもUlの子要素として呼ばれることを知っているとも言えます。

これが「親や子の要素をお互いが知っている」ということです。

const UserCardList = ({ users }: Props) => {
  return (
    <ul>
      {users.map((user) => (
        <UserCard user={user} />
      ))}
    </ul>
  )
}

const UserCard = ({ user }: Props) => {
  return (
    <li key={user.id} >
      <p>{user.id}</p>
      <p>{user.name}</p>
    </li>
  )
}

このような実装になっていると以下のような理由で使い回しづらいコンポーネントになります。

  • 呼び出し側も呼び出される側も、そのコンポーネントがどのように作られているかを気にする必要があること。
  • 呼び出される親が条件に合っていなければ使いまわせないこと。
    • 上の例のUserCardの場合、Ul以外の親から呼び出したい時に使えません。

これらの問題は以下のように実装すると解決します。

const UserCardList = ({ users }: Props) => {
  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>
          <UserCard user={user} />
        </li>
      ))}
    </ul>
  )
}

const UserCard = ({ user }: Props) => {
  return (
    <div>
      <p>{user.id}</p>
      <p>{user.name}</p>
    </div>
  )
}

このように実装すれば、UserCardListは子要素をLiタグでくくっているので、UserCardがどんな要素でも問題なく、UserCardも親がUlでもdivでも問題ありません。 このように「親や子の要素をお互いが知っている」という状況を減らすことができれば、より汎用的なコンポーネントとなります。

おわりに

今回は共通UIコンポーネントについて書かせていただきました。 この内容は共通UIコンポーネントの実装以外にも活かせる考えだと思います。 コンポーネントが、どこでどうやって使われるのか、今後どのように拡張する可能性があるのか、今の実装だと今後こういう時に困るのではないか?など、実装時点では分からないことは多いですが、デザイナーとコミュニケーションを多く取るなど、出来るだけ多くの情報を集め、対応できる幅が増えるように実装することを自分も意識し続けていきたいと思います。

エンジニアとして、やりがいのある仕事がしたい!わくわくする仕事をしたい!という方がいらっしゃいましたらぜひ、弊社採用窓口までご連絡ください。

TUNAGの開発体制、使用技術については下記ブログ記事を参照ください。 TUNAGの技術と開発体制のすべてを紹介します!

株式会社スタメンでは、オンラインサロン用プラットホームFANTSというサービスも開発・運営しております。 FANTSの開発技術・開発組織を紹介します!

またスタメン Engineering Handbookとして体系的にまとめられたページも公開していますので、こちらも是非ご覧ください。