styled-componentsの採用と既存資産を捨てた理由

こんにちは。フロントエンドエキスパートチームの@nakajmgです。

私が所属しているフロントエンドエキスパートチームでは、現在 kintone の脱レガシーの一環として React + TypeScript 化に取り組んでいます。この取組の中で Scss で定義されている既存のスタイルを styled-components で書き直していくという決定をしました。

今回は styled-components の採用を決定するまでの過程や、既存の Scss ファイルの扱いについて検討した内容などを紹介します。

スタイル定義方法の検討

kintone にはユーザーが JavaScript でカスタマイズできる機能があり、ユーザーが行っているカスタマイズの中には、DOM 構造や CSS のクラス名に依存しているものもあります。このようなカスタマイズはサポートの対象外ではありますが、ユーザーにできるだけ不利益を出したくないという考えから、かなり慎重な方法も検討しました。

  • React コンポーネントで DOM 構造とクラス名を再現して既存のスタイル(Scss)をそのまま使う
  • Scss ファイルからコンポーネントで使うスタイルを抜き出して、コンポーネントと対になる Scss ファイルを新規で作成する
  • 既存の Scss を使わず新たにスタイルを定義し直す

これらは検討した案の一部です。プロダクトがすでにリリースされて多くの人に使われているという状況と、どういった形でリリースしていくかを考えて、これ以外にも様々なパターンが挙げられました。

既存の Scss ファイルを捨てる

さまざまなパターンを検討した結果、スタイルについては再利用せずに書き直そうという決定になりました。

これは既存の Scss が@mixin@extendを多用していて、かなり複雑な状態になっていたこと、それが課題として認識されてたのが大きな要因です。課題を解決するより新しく書くほうがコストが低いと考えてこのタイミングで捨てることを決断しました。

また、DOM 構造についても既存のものを踏襲せずに、よりよいマークアップで作り変えていこうとなりました。

スタイルを何で定義するか

既存の Scss ファイルを使わないことが決まったので、次は React コンポーネントでスタイルを定義する方法です。

React コンポーネントのスタイルの定義方法については、次の3つの候補が挙げられました。

  • Scss ファイルで新規作成
  • Scss + CSS Modules
  • CSS in JS

Scss ファイルで新規作成

既存メンバーが慣れ親しんだ方法で行い、コンポーネントごとのスタイルを@extendなどを使わずに、現状より独立した形で捨てやすい単位で定義し直すという選択肢です。

この方法は React 化が完了していない部分のスタイルと相互に干渉しあう可能性があること、コンポーネント化の恩恵を受けづらいことを理由に採用しませんでした。既存の Scss の課題をどうにかしたいと思っていたのもあり、メンバーからは新たな課題を生み出すだけではという懸念が出ました。

CSS Modules ( with Scss )

Scss で挙げたような懸念を一部解消できる選択肢として CSS Modules(css-loader による利用) も候補に挙がりました。.scssをコンポーネントからimportして使うことで scoped なスタイルとして使えること、メンバーが持っている知識で運用できるという点では悪くない選択肢のように思いました。

しかしながら、現在 CSS Modules はメンテナンスオンリーな状態にあり、将来 deprecateとされる可能性が高く、採用するにはリスクが高いと判断しました。

また、.scss ファイルを ES Modules の構文で import するのは、ビルドツールの選定において CSS Modules をサポートしていることが必要条件となってしまうので、将来的に身動きがとりづらい状況になってしまう可能性があります。

CSS in JS

CSS in JS はその名のとおり JavaScript の中で CSS を書くものです。CSS in JS を実現するためのツールは多くありますが、代表的なものとして styled-components や emotion があります。

今回 styled-components を採用することにしましたが、次のような懸念点も挙げられました。

  • JavaScript(TypeScript)の中でスタイル書いていくのつらそう
  • styled なラッパーコンポーネントとかがいっぱい作られていろいろつらそう
  • JSX に styled なコンポーネントと React コンポーネントが混ざって視認性が悪くなりそう
  • デザイナーが参加しにくくなりそう
  • 負債になりそう

検討を重ねた結果これらの懸念点のほとんどは styled-components の使い方にルールを設けることで払拭できそうだと考えました。

styled-components の使用ルールを定める

styled-components を使う上で定めたのは次のようなルールです。

  • styledはコンポーネントを引数に取ってスタイルをあてる以外の使い方をしない
  • styledは同一ファイル内で使用してファイルを分割しない

具体的には次のような書き方になります。

import React from "react";
import styled from "styled-components";

const Component = ({ className }) => (
  <div className={className}>
    <div className={`${className}__child`}>コンポーネント</div>
  </div>
);

const StyledComponent = styled(Component)`
  background-color: #fff;
  color: #000;
  &__child {
    border: 1px solid #000;
  }
`;

export const MyComponent = StyledComponent;

JavaScript の中でスタイルが書くのがつらそうという懸念については、IDE やエディタにプラグインを入れることで軽減できます。また、この形式であれば styled なコンポーネントと React コンポーネントがごちゃ混ぜになることはありません。React コンポーネント自体もピュアな React コンポーネントの状態を保つことができます。

デザイナーがスタイルの修正などに参加しにくくなりそうという点も、スタイルが記述されている場所が限定されることで、これまでの .scss ファイルと同じように変更を加えられるようになっています。

そして将来的に負債になりそうという懸念ですが、これはどんなツールやライブラリを選んだとしても、早かれ遅かれ、多かれ少なかれ負債になるものだと考えています。その負債を小さくするために考えるべきは、スタイルを小さい範囲で書いて捨てやすい状況にすること、そして変更に耐えれる設計にできるかどうかです。この観点からすると、styled-components でも前述したルールで運用すれば大丈夫であろうと判断しました。

Scss の資産と向き合う

styled-components でやっていくにあたり、既存の Scss に定義してある変数をどうするかについて検討しました。

最終的な決定としてはこの機会に Scss の資産をすべて放棄して、styled-components に適したやり方で色などを管理していこうとなりました。styld-componets はテーマによってスタイルを変更する機能を持っているので、テーマとして扱っていくことにしました。

検討の途中で Scss の変数を抽出して JavaScript から扱えるように変換する方法を試してみました。このプロジェクトでは使わないことにしましたが、移行の途中段階などで使える状況もあるかと思いますので、方法を紹介します。

Scss の変数を JavaScript に変換する

次のようなディレクトリ構成をもとに変換方法を紹介します。

.
├── package-lock.json
├── package.json
└── src
    └── scss
        ├── _components.scss
        ├── _variables.scss
        └── entry.scss

変換には次のパッケージを使います。

npm からインストールしてください。

npm i -D sass-extract sass-extract-js rgb-hex

変換スクリプトの作成

次の内容を extractScssVariables.js としてプロジェクトのルートに作成します。

const fs = require("fs");
const path = require("path");
const sassExtract = require("sass-extract");
const rgbHex = require("rgb-hex");
const { writeFile } = fs.promises;

const extract = () => {
  const rendered = sassExtract.renderSync(
    {
      file: path.resolve(__dirname, "./src/scss/entry.scss"),
    },
    {
      plugins: [
        {
          plugin: "sass-extract-js",
          options: { camelCase: false },
        },
        {
          plugin: {
            run: () => ({
              postExtract: (vars) =>
                Object.keys(vars).reduce((ret, key) => {
                  ret[`$${key}`] = `#${rgbHex(vars[key])}`;
                  return ret;
                }, {}),
            }),
          },
        },
      ],
    }
  );
  console.log(rendered.vars);
  writeFile(
    path.resolve(__dirname, "./src/variables.js"),
    `
    export const scssVariables = ${JSON.stringify(rendered.vars, null, 2)}
  `.trim()
  );
};

extract();

Scss の中身

今回サンプルとして使うのは次のような Scss です。

// src/scss/_variables.scss
$color-text: #333;
$color-text-hover: #666;
$color-active: #3498db;
$color-border-focus: $color-active;
$bg-button: #f7f9fa;
$bg-button-hover: $color-active;
// src/scss/_components.scss
@import "./variables";
.button {
  color: $color-text;
  background-color: $bg-button;
  &:hover {
    color: $color-text-hover;
    background-color: $bg-button-hover;
  }
  &:focus {
    border-color: $color-border-focus;
  }
}
// src/scss/entry.scss
@import "./components";

これらの Scss をもとに変換します。

変換してみる

次のコマンドをプロジェクトのルートディレクトリで実行します。

node ./extractScssVariables.js

変換結果

実行が終わると、次のようなファイルが生成されます。

// src/variables.js
export const scssVariables = {
  "$color-text": "#333333",
  "$color-text-hover": "#666666",
  "$color-active": "#3498db",
  "$color-border-focus": "#3498db",
  "$bg-button": "#f7f9fa",
  "$bg-button-hover": "#3498db",
};

TypeScript で補完を効かせたい場合には次のように as const を足してください。

writeFile(
  path.resolve(__dirname, "./src/variables.js"),
  `
  export const scssVariables = ${JSON.stringify(
    rendered.vars,
    null,
    2
  )} as const
`.trim()
);

これで Scss の変数が JavaScript から扱える形になりました。直接importrequireをして使ったり、styled-components のthemeで注入して使うといったことが可能になります。

今回変換対象として用意した Scss ファイルはシンプルなものでしたが、このスクリプトはエントリーとなる Scss から辿れる全ての変数を抽出してくれます。

今回紹介したスクリプトのサンプルが次のリポジトリに置いてありますので、ぜひお手元の Scss ファイルで試してみてください。

https://github.com/nakajmg/sample-scss-variables-to-js

おわりに

styled-components の採用を決めた経緯と、既存の Scss をどう扱っていくかについて紹介しました。

負債はコツコツと返済していければ良いですが、対象やタイミングによっては難しいこともあるので、思い切って捨ててしまう(作り直す)という選択も有効です。

プロジェクトの状況やチームが達成したい目標によってレガシーとの向き合い方はさまざまですが、先を見据えてじっくりと検討を重ねて、一歩づつ進んでいければと思います。