📑

React Component 分業の覚書

2021/01/30に公開

フロントエンドを Next.js 化する機会が多くなってきた昨今。いざ取り組むにしても、スタイリング込みで実装出来るエンジニアが不足気味ではないでしょうか?また、縦割り分業している現場ではこれまで、マークアップ(フロントエンド)エンジニアが html + css を納品し、それを元にサーバーサイドエンジニアがテンプレートエンジンに組み込むという業務フローも少なくありませんでした。

この様な業務フローの場合、同じリポジトリで作業してもらうという事が難しいこともあります。Next.js 移行期のこれからも同様のことが多々起きると予想しており、完全分業するうえでの最適化を考える必要があります。この件について少しまとめたくなったので、メモ書きとして残します。

前提条件

以下の座組みは React Component 分業で最適だと考えている組み合わせです。マークアップエンジニアはこれまでと変わらず、ちょっとしたガイドラインを覚えるだけです。マークアップが出来るデザイナーでも参画できるスタックなので、ハードルが低いです。

  • マークアップエンジニア:html + scss
  • フロントエンドエンジニア:Next.js + CSS Modules

BEM の Block 粒度は Component 粒度そのもの

React Component の粒度と、BEM の Block 粒度はとても似ています。よく見慣れたヘッダーコンポーネントのスタイリングは、次の様なものです。この時、Header という Block 名はそのまま React Component 名とすることができます。

Header.scss
.Header {
  &__nav {
    ...;
  }
  &__logo {
    ...;
  }
}

フロントエンドエンジニアは納品された scss ファイルを少し調整。 CSS Modules のローカルスコープハッシュが Block の役割を担ってくれるので、Element を取り出すだけです。BEM を採用していれば、React Component 化を素早く行うことができます。

styles.module.scss
.nav {
  ...;
}
.logo {
  ...;
}
Header.tsx
import styles from "./styles.module.scss";

function Header() {
  return (
    <header>
      <h1 className={styles.logo}></h1>
      <nav className={styles.nav}></nav>
    </header>
  );
}

これだけで React Component に置き換えることが出来ました。納品物のアークアップが以下の様なものになるはずなので、機械的に変換することも出来そうです。

header.html
<header class="Header">
  <h1 class="Header__logo"></h1>
  <nav class="Headder__nav"></nav>
</header>

適用しやすい BEM の書き方

1.Block をアッパーキャメルケースで書く

React Component は アッパーキャメルケースで Component 命名をします。そのため、納品されるセレクタ Block がアッパーキャメルケースであれば、Component 命名をそのまま採用することもでき、脳内変換が容易になります。

2.Element をローワーキャメルケースで書く

ローワーキャメルケースが一般的かと思いますが、念のため。スネークケースを JS(TS)コードに混在させたくない気持ちがフロントエンド側は強いです。また適用しやすいという話とは別ですが、Element をネストする記法は書き方として間違っているのでやめましょう。

3.Modifier は data 属性を活用する

マークアップ・スタイリングを依頼する時、重要なガイドラインが Modifier です。style を微調整する Modifier を CSS Modules では表現しづらく、以下の定義をそのままマッピングすることが出来ません。

Header.scss
.Header {
  &__nav {
    &--open {
      /* NG */
    }
    &.is-open {
      /* NG */
    }
  }
}

これを助けるのが data 属性によるスタイリングです。フロントエンドエンジニアはそのままマッピングをすることが出来ます。CSS Modules によるローカルスコープも適用できます。

Header.scss
.Header {
  &__nav {
    &[data-is-open] {
      /* OK */
    }
  }
}
header.html
<!-- NG -->
<header class="Header">
  <h1 className="Header__logo"></h1>
  <nav className="Headder__nav is-open"></nav>
</header>
<!-- OK -->
<header class="Header">
  <h1 className="Header__logo"></h1>
  <nav className="Headder__nav" data-is-open></nav>
</header>

React Component から見ても、マルチクラス指定よりも data 属性 Modifier の方が綺麗にかけ、clsx による合成なども不要です。

Header.tsx
import clsx from "clsx";
import styles from "./styles.module.scss";
function Header(props: { isOpenNav?: true; }) {
  return (
    <header>
      <h1 className={styles.logo}></h1>
      <nav className={
        clsx(styles.nav, props.isOpenNav && "is-open")
        {/* .is-open という class 名を得る事ができない */}
      }></nav>
    </header>
  );
}
Header.tsx
import styles from "./styles.module.scss";
function Header(props: { isOpenNav?: true; }) {
  return (
    <header>
      <h1 className={styles.logo}></h1>
      <nav
        className={styles.nav}
        data-is-open={props.isOpenNav}>
      </nav>
    </header>
  );
}

また、data 属性は値を受け取る事ができるため、theme 指定などにも適しています。React Component と data 属性スタイリングは相性が良いです。

Header.tsx
import styles from "./styles.module.scss";
function Header(props:
  { isOpenNav?: true; theme: 'light' | 'dark'; }
) {
  return (
    <header>
      <h1 className={styles.logo}></h1>
      <nav
        className={styles.nav}
        data-is-open={props.isOpenNav}
        data-theme={props.theme}
      >
      </nav>
    </header>
  );
}

Element を取り出し、CSS Modules として変換された指定は、完結で理解しやすいものとなりました。

styles.module.scss
.nav {
  &[data-is-open] {
  }
  &[data-theme="light"] {
  }
  &[data-theme="dark"] {
  }
}

React だけでなく従来のテンプレートエンジンでも、data 属性を活用したスタイリングの方がコードの可読性が高くなるため、良い習慣になると思います。

Discussion