Mercari Engineering Blog

We're the software engineers behind Mercari. Check out our blog to see the tech that powers our marketplace.

styled-componentsによる抽象コンポーネント作成のすゝめ

この記事は MERPAY TECH OPENNESS MONTH の 11 日目の記事です。

こんにちは、メルペイのフロントエンドエンジニアの @sawa-zen です。本記事では React ベースのプロジェクトでのコンポーネント作成をちょっと楽するテクニックをご紹介します。

課題:コンポーネントのスタイル重複問題

サービスやツールの開発をしていると多くのコンポーネントを実装することになります。その際に同じようなスタイルを何度も記述することになりイライラした経験ありませんか? 例えば margin: 0;padding: 0;box-sizing: border-box; などなど。プロジェクトが大きくなればなるほどこの面倒な作業が増えていきます。

解決策 1 :グローバルへリセット CSS を定義する

グローバルへリセット CSS を定義して一掃してしまうのも一つの手です。例えば以下のように。

// index.html
<!-- 省略 -->

<style>
div, main, article /*, etc... */ {
  display: flex;
  box-sizing: border-box;
  margin: 0;
  padding: 0;
 /* etc... */
}
</style>

<!-- 省略 -->

この方法はあるプロジェクト A の中だけで完結するのであれば問題無いかもしれません。しかし、他プロジェクト B でも使用するとなった時、A のグローバルで定義した CSS を B にも適用しなくてはなりません。これは B のスタイル崩れを起こす可能性があり危険です。そもそも、グローバル CSSへ依存したコンポーネントの実装は堅牢な設計とは言えません。

解決策 2 :抽象コンポーネントを使い回す

もう一つのアプローチとして元々それらの CSS が当てられた抽象コンポーネントを作成します。例えば以下のように。

// View.jsx
import React from 'react';

const baseStyle = {
  display: 'flex',
  boxSizing: 'border-box',
  margin: 0,
  padding: 0,
};

const View = ({ children, style = {}, ...other }) => (
  <div 
    style={{
      ...baseStyle, 
      ...style,
    }} 
    {...other}
  >
    {children}
  </div>
);

export default View;

この View コンポーネントをベースとして実装していくことで重複した CSS を何度も書くことから開放されました。ですが実はこれも難ありです。

要素が固定されてしまう

View コンポーネントは div 要素をベースとして作成されたため、View コンポーネントで記述した箇所は全て div 要素としてレンダリングされてしまいます。 SEOやアクセシビリティの観点から見てもNGです。適切な要素を使うことができなければとても採用できません。

しかしこの問題は styled-components を使用することで解決できます。

styled-componentsas を使った要素の切り替えができる

styled-components のv4系から as プロパティが追加されました。as プロパティは変更したい要素名を渡すことで自由に要素を変更できます。styled-components はテンプレートリテラルを使って本来のCSSの記法でCSS in JSを実現することができるライブラリです。

www.styled-components.com

まずは先程の View コンポーネントを styled-components を使って を書き換えてみます。

// View.jsx
import styled from 'styled-components';

const View = styled.div`
  display: flex;
  box-sizing: border-box;
  margin: 0;
  padding: 0;
`;

export default View;

呼び出し側で以下のようにas プロパティを使って div から main に変更してみます。

// App.jsx
import React from "react";
import View from './View';

const App = () => (
  <div>
    <View as="main">これはmain要素です</View>
  </div>
);

export default App;

divがmainに変わった様子
divがmainに変わった様子

無事 divmain へと姿を変えました。今後はこの View.jsx ベースにすることであたかもリセットCSSがあたっているかのような状態で実装することができます。

型は大丈夫?

残念ながら現時点(2019/06/03) では as を使用した場合以下のようなコードは型エラーになります。

<View as="img" src="./hoge.png" alt="これはimg要素です" />

div を前提としたpropしか渡せないためsrcaltが型と一致せずエラーになります。これに関して型定義ファイル上に「タイプセーフにしたいなら as を使用せず withComponent を今は使ってください」とコメントが残っていました。

Typing Note: prefer using .withComponent for now as it is actually type-safe.

github.com

withComponent は以前から実装されており、 as と同様の恩恵を受けられますがv4から非推奨になっています。そのため as の型定義の修正が急がれています。ちなみに withComponent を使う場合は以下のようなコードになります。

// ./App.jsx
import React from "react";
import View from './View';

const ImgView = View.withComponent('img');

const App = () => (
  <div>
    <ImgView src="./hoge.png" alt="これはimg要素です" />
  </div>
);

export default App;

as よりも少し冗長になってしまうので、できれば as を使いたいところです。

おまけ

個人的には View の他にもベースとなるコンポーネントをいくつか定義しておくことをおすすめします。今回 View というコンポーネント名にしたのはReactNativeのコンポーネントを参考にしています。ブロック要素系のベースコンポーネントとしてViewを、インライン要素系をText、画像を Imageというように用途に応じていくつか分割しておいた方が可読性も上がりスタイルの書き換えも楽なのでおすすめです。

まとめ

今回の手法を用いることで重複して面倒だった CSS を記述することなく、堅牢且つ効率的に実装できるようになりました。 本記事では styled-components をベースにお話しましたが、emotion などの同様の機能を持った類似ライブラリでもかまいません。 型定義に一部問題はありましたが、近い内に解決されると思うのでTypeScriptを使ったプロジェクトでもどんどん使っていきましょう!

次はnerocruxさんによる「WebAuthn での認証サーバー実装について(仮)」です。お楽しみに!

メルペイではフロントエンドエンジニアを募集しています!一緒に働ける仲間をお待ちしております。