クモのようにコツコツと

フロントエンドエンジニア イイダリョウの技術ブログ。略称「クモコツ」

【React】styled-componentsのcss helperでスタイルを継承(Reactとメタ言語の比較-5)

Reactの続きです。前回はstyled-componentsのcreateGlobalStyleでbodyタグにCSS設定をしました。今回はcss helperでスタイルを継承します。前回一回リセットされたCSSスタイルは今回復活できました♪Sass(SCSS)との比較も行います。それではいきましょう!

【目次】

※参考:前回記事
【React】styled-componentsのcreateGlobalStyleでbodyタグにCSS設定(Reactとメタ言語の比較-4) - クモのようにコツコツと

※参考:Reactを習得するためにやったことまとめ
qiita.com

前回のおさらい

styled-componentsのcreateGlobalStyleでbodyタグにCSS設定 https://cdn-ak.f.st-hatena.com/images/fotolife/i/idr_zz/20210209/20210209194247.jpg

いったんCSSスタイルはすべてリセットしたが、コンポーネントの外のbodyタグにスタイルを設定することはできた。

※参考:【React】styled-componentsのcreateGlobalStyleでbodyタグにCSS設定(Reactとメタ言語の比較-4) - クモのようにコツコツと

既存コンポーネントにスタイル設定(エラー)

次はいよいよコンポーネントにスタイルを当ててみる!

まず、「App.tsx」にstyled-componentsをインポート

import styled from 'styled-components';

変数HeaderにHeaderコンポーネントのスタイルを設定

const Header = styled.header`
background: "#eee";
`;

そうすると下記のHeaderコンポーネントにスタイルが当たるはず

function App() {
  return (
    <div className="App">
        <Helmet
            title={ titleText }
            meta={[
                { name: 'description', content: descriptionText }
            ]}
         />
        <Header />
        <Main />
        <Footer />
    </div>
  );
}

アプリ起動

$ npm start

ありゃ、こんなエラーが…

./src/App.tsx
SyntaxError: /(パス)/react-from-meta-lang/src/App.tsx: Identifier 'Header' has already been declared (12:6)

「識別子Headerはすでに宣言されています」と。

確かにHeaderは「Header.tsx」の中でも関数コンポーネントとして設定している。

function Header() {
  return (
    <header className="header">
        <h1 className="header__title">{ title }</h1>
        <p className="header__text" dangerouslySetInnerHTML={{ __html: text }}></p>
    </header>
  );
}

これと重複するということか。

Headerコンポーネント内でスタイルを設定してみる(エラー)

同じことをHeaderコンポーネント内でやってみる(ほぼダメ元、確認作業w)。

「Header.tsx」でstyled-componentsをインポート

import styled from 'styled-components';

Headerスタイルを設定

const Header = styled.header`
background: #eee;
`;

結果は同じエラー

./src/Header.tsx
SyntaxError: /(パス)/react-from-meta-lang/src/Header.tsx: Identifier 'Header' has already been declared (12:9)

でしょうねw

同じファイルでもコンポーネントと名前が被っているのは変わらないからな。

function Header() {
  return (
    <header className="header">
        <h1 className="header__title">{ title }</h1>
        <p className="header__text" dangerouslySetInnerHTML={{ __html: text }}></p>
    </header>
  );
}

なお、headerタグをHeaderコンポーネント化してみても結果は同じエラーだった

function Header() {
  return (
    <Header>
        <h1 className="header__title">{ title }</h1>
        <p className="header__text" dangerouslySetInnerHTML={{ __html: text }}></p>
    </Header>
  );
}

関数コンポーネントと別名のコンポーネントにスタイルを当てる(成功!)

コンポーネントの名前被り問題、いやー、どうしたものか。わざわざstyled-componentsのために別名のコンポーネントを当てるのかな?例えばこんな感じに?

function App() {
  return (
    <div className="App">
        <Helmet
            title={ titleText }
            meta={[
                { name: 'description', content: descriptionText }
            ]}
         />
        <HeaderStyle>
            <Header />
        </HeaderStyle>
        <Main />
        <Footer />
    </div>
  );
}

それってあまりエレガントではないな…。

いや、まてよ、そもそもこの<Header />ってただ配置しているだけで、実際のスタイルをここに当てたいわけではないよな?


実際にスタイルを当てたいのはJSXの中のheaderタグなんだよな。

function Header() {
  return (
    <header>
        <h1 className="header__title">{ title }</h1>
        <p className="header__text" dangerouslySetInnerHTML={{ __html: text }}></p>
    </header>
  );
}

そしたらこのheaderタグをHeaderとは別名のコンポーネントにすればいいのでは?


スタイル設定を変数HeaderTagに変更してみる。

const HeaderTag = styled.header`
background: #eee;
`;

headerタグもHeaderTagコンポーネントにしてみる。

function Header() {
  return (
    <HeaderTag>
        <h1 className="header__title">{ title }</h1>
        <p className="header__text" dangerouslySetInnerHTML={{ __html: text }}></p>
    </HeaderTag>
  );
}

ブラウザを見ると、おお、スタイルに当たった! f:id:idr_zz:20210211141230j:plain

「HeaderTag」コンポーネントはheaderタグになってランダムなclass名.kYPuzKが当たっている! f:id:idr_zz:20210211141248j:plain よし、このやり方でいくか!

mixin設定の外部ファイルを作る

前回のCSS変数設定を「css-variables.json」という別ファイルで作った。コンポーネントを跨いで使いたかったので。mixin設定も同様なので別ファイルにしたい。

まず「style」フォルダを作って「css-variables.json」をその中に入れる。ファイル名は「variables.json」に改名。このファイルのインポート部分のパスも修正した。

修正前

import cssVariables from './css-variables.json';

修正後

import cssVariables from './style/variables.json';

次にこの「style」フォルダの中に「mixin.ts」を作成。

なお、JSXコンポーネントがないファイルの拡張子は「.tsx」ではなく「.ts」でいくことにした。

※参考:TypeScript - Reactを使うプロジェクト内のTypeScriptをすべて.tsxで作るデメリットについて|teratail


メタ言語スターターキットのSass(SCSS)では「_mixin.scss」にmixinを設定していた。

@mixin pageSize($width: 1000px) {
    width: 100%;
    max-width: $width;
    padding: 20px;
    margin: 0 auto;
}

このスタイルをモジュールを超えて継承させてた。

※参考:【メタ言語】フロントエンド開発スターターキットを作った(EJS、Sass(SCSS)、TypeScript) - クモのようにコツコツと

css helperで継承スタイルを設定

styled-componentsのcss helper関数を使うとスタイルの継承ができる。

※参考:styled-components: API Reference
※参考:styled-components の mixin としての使い方が便利そう - Qiita

「mixin.ts」でstyled-componentscss機能をインポート

import styled, { css } from 'styled-components';

変数pageSizecssを設定。タグは指定不要!

const pageSize = css`
    width: 100%;
    max-width: 1000px;
    padding: 20px;
    margin: 0 auto;
`;

pageSizeをエクスポート

export {pageSize};

{}でくくると複数の変数や関数をエクスポートできる(今はpageSizeだけしか設定していないが、のちのちmixin全体はこの中にまとめる想定)。

※参考:ファイルモジュールの詳細 - TypeScript Deep Dive 日本語版

※参考:逆引きstyled-components 共通コンポーネントをうまく作る5つの小技 - mottox2 blog

pageSize設定を継承

「Header.tsx」で「mixin.ts」の中からpageSizeをインポートする

import { pageSize } from './style/mixin';

HeaderTagpageSizeを読み込む

const HeaderTag = styled.header`
  ${pageSize}
  background: #eee;
`;

前回の変数と同様、継承でもプレースホルダー${ }を使う。


ブラウザ確認 f:id:idr_zz:20210211142554j:plain おお!Headerだけmixinのスタイルが当たった!

Headerコンポーネントのスタイルを仕上げる

SCSS(Sass)の「_header.scss」は下記のようなスタイル設定だった

/* header */
.header {
    @include pageSize();
    text-align: center;
    background: $bg-color_g;
    padding: 20px;
    &__title {
        font-size: 2em;
    }
}

背景色以外の設定も追加していく。


変数を読み込むので前回と同様に「variables.json」をインポート

import cssVariables from './style/variables.json';

const variable = cssVariables.variable;

HeaderTagにスタイル追加、プレースホルダーで変数bgColor_gも読み込む。

const HeaderTag = styled.header`
  ${pageSize}
  text-align: center;
  background: ${variable.bgColor_g};
  padding: 20px;
  & h2 {
    font-size: 2em;
  }
`;

Sass(SCSS)の時に命名したBEMのclass名を無くしてみる。

function Header() {
  return (
    <HeaderTag>
        <h2>{ title }</h2>
        <p dangerouslySetInnerHTML={{ __html: text }}></p>
    </HeaderTag>
  );
}

ブラウザ確認 f:id:idr_zz:20210211142842j:plain ヘッダーのスタイル完成!

h2やpタグにはclass名はないが… f:id:idr_zz:20210211142902j:plain

headerタグのランダムなclass名.dacmuKのスコープになっている! f:id:idr_zz:20210211142923j:plain

Mainコンポーネントのスタイルを設定

次は「Main.tsx」を修正する。

修正前のMainコンポーネントのJSX

function Main() {
  return (
    <main>
      <section className="main">
          <h1 className="main__title">{ title }</h1>
          <p className="main__title" dangerouslySetInnerHTML={{ __html: text }}></p>
          <Inner />
      </section>
    </main>
  );
}

SCSS(Sass)の「_main.scss」はこのような設定だった

.main {
    @include pageSize();
    &__title {
      font-size: 1.5em;
    }
  }

ここでもpageSizeを継承している。


styled-componentspageSizeをインポート

import styled from 'styled-components';
import { pageSize } from './style/mixin';

SectionTagでのスタイル設定、pageSizeを継承。

const SectionTag = styled.section`
  ${pageSize}
  & h1 {
    font-size: 1.5em;
  }
`;

変更後のMainコンポーネント

function Main() {
  return (
    <main>
      <SectionTag>
          <h1>{ title }</h1>
          <p dangerouslySetInnerHTML={{ __html: text }}></p>
          <Inner />
      </SectionTag>
    </main>
  );
}

sectionタグをSectionTagコンポーネントにして、h1タグ、pタグはBEMのclass名を削除。


ブラウザを確認 f:id:idr_zz:20210211143219j:plain やった、本文にもスタイルが当たった!

sectionタグにランダムなclass名.fjwMFHがあたって… f:id:idr_zz:20210211143236j:plain

h1タグがスコープになっている♪ f:id:idr_zz:20210211143254j:plain

Innerコンポーネントのスタイル設定

次は「Inner.tsx」。以前、本文の中でmap()でループしたsectionタグ部分。

※参考:【React】JSONデータをJSXに読み込んで表示する(Reactとメタ言語の比較-3) - クモのようにコツコツと

変更前のInnerコンポーネントのJSX

function Inner() {
  return (
    <div className="inner">
      {innerJson.map((innerJson, index) => 
        <section key={ index }>
          <h2 className="inner__title">{ innerJson.title }</h2>
          <p className="inner__text" dangerouslySetInnerHTML={{ __html: innerJson.text }}></p>
        </section>
      )}
    </div>
  );
}

Sass(SCSS)の「_inner.scss」

.inner {
    &__title {
      font-size: 1.25em;
      color: $base-color;
    }
}

今回は変数設定のみか。


styled-componentscssVariablesをインポート

import styled from 'styled-components';
import cssVariables from './style/variables.json';

const variable = cssVariables.variable;

SectionTagでsectionタグのスタイル設定、変数baseColorも読み込み。

const SectionTag = styled.section`
  & h2 {
    font-size: 1.25em;
    color: ${variable.baseColor};
  }
`;

スタイルはh2タグのみだが、今後の拡張を考慮して外側のsectionタグをコンポーネント化した。


変更後のJSX。sectionタグをSectionTagコンポーネントにしてBEMのclass名は削除。

function Inner() {
  return (
    <div className="inner">
      {innerJson.map((innerJson, index) => 
        <SectionTag key={ index }>
          <h2>{ innerJson.title }</h2>
          <p dangerouslySetInnerHTML={{ __html: innerJson.text }}></p>
        </SectionTag>
      )}
    </div>
  );
}

ブラウザ確認 f:id:idr_zz:20210211143715j:plain Innerコンポーネントにスタイルが当たった!

sectionタグにランダムなclass名.iGTxTzが当たっている f:id:idr_zz:20210211143730j:plain

h2タグが.iGTxTzのスコープになっている f:id:idr_zz:20210211143746j:plain

Footerコンポーネントのスタイル設定

最後、「Footer.tsx」コンポーネントにスタイルを設定する。

変更前のFooterコンポーネントのJSX

function Footer() {
  return (
    <footer className="footer">
        <p className="footer__text" dangerouslySetInnerHTML={{ __html: text }}></p>
    </footer>
  );
}

Sass(SCSS)の「_footer.scss」

.footer {
    @include pageSize();
    text-align: center;
}

mixinでpageSizeを読み込んでいる。変数はなし。


styled-componentspageSizeをインポート

import styled from 'styled-components';
import { pageSize } from './style/mixin';

FooterTagでfooterタグのスタイル設定。pageSizeを継承。

const FooterTag = styled.footer`
  ${pageSize}
  text-align: center;
`;

変更後のJSX、footerタグをFooterTagコンポーネントに。

function Footer() {
  return (
    <FooterTag>
        <p dangerouslySetInnerHTML={{ __html: text }}></p>
    </FooterTag>
  );
}

ブラウザ確認 f:id:idr_zz:20210211145844j:plain よっしゃ!フッターもCSS復活〜♪

footerタグにランダムなclass名.TbtuHが当たっている f:id:idr_zz:20210211145904j:plain

.TbtuHにはpageSizeFooterTagのスタイルが当たっている f:id:idr_zz:20210211145917j:plain


CSS設定は別ファイルではなく、ページ内のheadタグの中にあるようだった。 f:id:idr_zz:20210211180811j:plain

ファイル、フォルダを整理

ファイルがだいぶ増えてきたので整理したい。

まず「data」フォルダを作成し「data.json」をこの中に。インポートのパスも変更。

import Data from './data/data.json';

「modules/hello」フォルダを作り、「hello.js」をその中に入れる。インポートにパスも変更。

import './modules/hello/hello';

元々あった「App.css」と「index.css」はもう使わないので削除しちゃう。


コンポーネント以外のファイルがフォルダの中に格納された。 f:id:idr_zz:20210211150430j:plain


画面確認、問題ナシ! f:id:idr_zz:20210211145844j:plain


メタ言語スターターキットではEJSと f:id:idr_zz:20210211150725j:plain

Sass(SCSS)がほぼ同じ構成だった f:id:idr_zz:20210211150741j:plain

※参考:【メタ言語】フロントエンド開発スターターキットを作った(EJS、Sass(SCSS)、TypeScript) - クモのようにコツコツと


Reactではstyled-componentsによってCSSがコンポーネントと一体化した。 f:id:idr_zz:20210211150921j:plain
ファイル構成がシンプルになった!


ソース(GitHub)

※参考:GitHub - ryo-i/react-from-meta-lang at 18ecb014d211d835aa99c8bfc26118df985c8462

プレビュー(GitHub Pages)

※参考:Reactとメタ言語の比較

最後に

ということで、styled-componentsによって無事にCSS設定が復活しましたー。変数や継承によってコンポーネントを超えた共通の値を設定することができました。

ファイル構成はEJSとSass(SCSS)の時はほぼ同じモジュール構成が二重に設定されており、モジュール構成を変えるたびに二箇所を変更する必要がありました。styled-componentsを使うとJSXとCSS in JSが同じファイルに設定できるので、ファイル構成はシンプルになりました!

また、styled-componentsがつけるランダムなclass名がスコープになるのでBEM記法のclass名は不要になりました。CSSの設定自体はSass(SCSS)とほぼ同じ書き方ができました。

CSS編はここまで。次回は「こんにちは、ふろんとえんど」というメッセージを表示しているJS部分をTypeScriptのモジュール化していきます。

それではまた!


※参考:Reactを習得するためにやったことまとめ
qiita.com