なにこれ

Gatsbyでブログを作っていて、機能拡張とともにテンプレートが肥大化してきたので、下記図の青枠単位でコンポーネント分割しました。 あわせてCSS Moduleも採用しました。そのときのメモです。

分割した結果

CSSをコンポーネントスコープにする方法

コンポーネントを分割する際、CSSもあわせて分割が必要です。その場合、グローバルスコープだとスタイルがコンポーネントに限定されなくなってしまうので、コンポーネントスコープにする必要があります。Gatsbyはコンポーネントスコープの実現方法が3つ用意されています。

僕はスタイルをHTMLやJSの中に書くのを躊躇してしまう人(古い時代の人)なので、スタイルをCSSファイルに書けるCSS Moduleを採用しました。 流行はStyled Componentsなんでしょうか?

CSS Module

CSSファイルのクラス名、アニメーション名をローカルスコープで使えるようにする仕組みです。 CSSファイルは通常のCSSと同じ文法で書くことができて、ICSSまたはInteroperable CSSと呼ばれる形式にコンパイルすることでローカルスコープを実現します。 JavaScriptファイルでCSS Moduleを使う場合、ローカル変数名をつけてインポートし、DOMのクラス名には変数名.クラス名のように指定します。

import styles from "./style.css";

element.innerHTML = '<div class="' + styles.className + '">';

参考:公式ページ

GatsbyでCSS Module

CSSファイル名をXXX.module.cssのようにしてコンポーネントでインポートするだけでCSS Moduleが使えます。

例えば下記のようなCSSがあるとしたら

src/components/container.module.css
.container {
  margin: 3rem auto;
  max-width: 600px;
}

JavaScriptでこのように読み込みます。

src/components/container.js
import React from "react"
import styles from "./container.module.css"

export default ({ children }) => (
  <div className={styles.container}>{children}</div>
)

コンポーネント分割

CSSがコンポーネントスコープにできたところで、ようやくコンポーネントを分割できるようになります。 純粋にReactの作法に従って分割します(Gatsbyは特に関係ありません)。
ここでは、テンプレートから記事部分を分割するケースについて説明しましょう。

テンプレート(分割前)

src/templates/blog-post.js
import styles from 'blog-post.module.css';

class BlogPostTemplate extends React.Component {

  render() {
    const post = this.props.data.markdownRemark

    return (
      <Layout>
        <h1 className={styles.blogTitle}>Takumon Blog</h1>
        <article>
          <h1 className={styles.postTitle}>{post.frontmatter.title}</h1>
          <div className={styles.postContent} dangerouslySetInnerHTML={{ __html: post.html }} />
        </article>
      </Layout>
    )
  }
}

export default BlogPostTemplate

export const pageQuery = graphql`
  query($slug: String!) {
    markdownRemark(fields: { slug: { eq: $slug } }) {
      html
      frontmatter {
        title
      }
    }
  }
`
src/templates/blog-post.module.css
.blogTitle {
  color: red
}

.postTitle {
  color: blue;
}

.postContent {
  color: green;
}

分割後は下記のようになります。 なお分割したコンポーネントに記事情報を渡すための属性を定義します。ここではpostという属性名にしています。
テンプレートにて記事コンポーネントのpost属性に記事情報を指定すると、 記事コンポーネントではthis.props.postで受け取れるようになります。

テンプレート(分割後)

src/templates/blog-post.js
import styles from 'blog-post.module.css';
import Post from '../components/post';

class BlogPostTemplate extends React.Component {

  render() {
    const post = this.props.data.markdownRemark

    return (
      <Layout>
        <h1 className={styles.blogTitle}>Takumon Blog</h1>
        <Post post={post} />
      </Layout>
    )
  }
}

export default BlogPostTemplate

export const pageQuery = graphql`
  query($slug: String!) {
    markdownRemark(fields: { slug: { eq: $slug } }) {
      html
      frontmatter {
        title
      }
    }
  }
`
src/templates/blog-post.module.css
.blogTitle {
  color: red
}

記事コンポーネント

src/components/post.js
import styles from 'post.module.css';

class Post extends React.Component {

  render() {
    const post = this.props.post;

    return (
      <article>
        <h1 className={styles.postTitle}>{post.frontmatter.title}</h1>
        <div className={styles.postContent} dangerouslySetInnerHTML={{ __html: post.html }} />
      </article>
    )
  }
}

export default Post
src/components/post.module.css
.postTitle {
  color: blue;
}

.postContent {
  color: green;
}

なお分割したことによりsrc/components/post.module.cssが「記事に対するスタイル」ということは自明になるので、クラス名をより簡潔にできます。

記事コンポーネント(CSSリファクタリング後)

src/components/post.js
import styles from 'post.module.css';

class Post extends React.Component {

  render() {
    const post = this.props.post;

    return (
      <article>
        <h1 className={styles.title}>{post.frontmatter.title}</h1>
        <div className={styles.content} dangerouslySetInnerHTML={{ __html: post.html }} />
      </article>
    )
  }
}

export default Post
src/components/post.module.css
.title {
  color: blue;
}

.content {
  color: green;
}

以上の手順を繰り返すことで、コンポーネントを細かく分割できます。

まとめ

コンポーネントを細かく分割することで、再利用性が向上し、画面レイアウトを組みやすくなりました。 ReactのWebアプリケーションの場合は、Reduxのステート用コールバック関数の受け渡しなどで複雑になりますが、 Gatsbyのような静的Webサイトの場合、単純な分割だけで事足りるかなーと。 分割設計についてはAtomic Design ~堅牢で使いやすいUIを効率良く設計するを参考にしたいところです。まだ読んでない...