この記事は 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-components
は as
を使った要素の切り替えができる
styled-components
のv4系から as
プロパティが追加されました。as
プロパティは変更したい要素名を渡すことで自由に要素を変更できます。styled-components
はテンプレートリテラルを使って本来のCSSの記法でCSS in JSを実現することができるライブラリです。
まずは先程の 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
へと姿を変えました。今後はこの View.jsx
ベースにすることであたかもリセットCSSがあたっているかのような状態で実装することができます。
型は大丈夫?
残念ながら現時点(2019/06/03) では as
を使用した場合以下のようなコードは型エラーになります。
<View as="img" src="./hoge.png" alt="これはimg要素です" />
div
を前提としたpropしか渡せないためsrc
と alt
が型と一致せずエラーになります。これに関して型定義ファイル上に「タイプセーフにしたいなら as
を使用せず withComponent
を今は使ってください」とコメントが残っていました。
Typing Note: prefer using .withComponent for now as it is actually type-safe.
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 での認証サーバー実装について(仮)」です。お楽しみに!
メルペイではフロントエンドエンジニアを募集しています!一緒に働ける仲間をお待ちしております。